From 000c7d98a62473ddcb8c2fba33278ffea9314dd4 Mon Sep 17 00:00:00 2001 From: tigasnogueira Date: Mon, 5 Dec 2022 11:30:45 -0300 Subject: [PATCH] teste final --- BackendTest.sln | 48 +++ src/.vscode/launch.json | 35 +++ src/.vscode/settings.json | 3 + src/.vscode/tasks.json | 41 +++ .../Configuration/ApiConfig.cs | 100 +++++++ .../Configuration/AutoMapperConfig.cs | 13 + .../DependencyInjectionConfig.cs | 30 ++ .../Configuration/IdentityConfig.cs | 53 ++++ .../Configuration/LoggerConfig.cs | 39 +++ .../Configuration/SwaggerConfig.cs | 153 ++++++++++ .../Controllers/AuthController.cs | 133 +++++++++ .../Controllers/BookController.cs | 80 +++++ .../Controllers/MainController.cs | 73 +++++ .../Data/ApplicationDbContext.cs | 13 + src/Book.Aplication/Extensions/AppSettings.cs | 10 + src/Book.Aplication/Extensions/AspNetUser.cs | 67 +++++ .../Extensions/CustomAuthorize.cs | 48 +++ .../Extensions/ExceptionMiddleware.cs | 32 ++ .../Extensions/IdentityMessagesPortuguese.cs | 28 ++ .../Extensions/SqlServerHealthCheck.cs | 35 +++ src/Book.Aplication/Program.cs | 40 +++ .../Properties/launchSettings.json | 31 ++ .../appsettings.Development.json | 18 ++ src/Book.Aplication/appsettings.json | 19 ++ src/Book.Application/Book.Application.csproj | 43 +++ .../Configuration/ApiConfig.cs | 100 +++++++ .../Configuration/AutoMapperConfig.cs | 13 + .../DependencyInjectionConfig.cs | 30 ++ .../Configuration/IdentityConfig.cs | 53 ++++ .../Configuration/LoggerConfig.cs | 39 +++ .../Configuration/SwaggerConfig.cs | 152 ++++++++++ .../Controllers/AuthController.cs | 133 +++++++++ .../Controllers/BookController.cs | 79 +++++ .../Controllers/MainController.cs | 73 +++++ .../Controllers/WeatherForecastController.cs | 32 ++ .../Data/ApplicationDbContext.cs | 13 + .../Extensions/AppSettings.cs | 10 + src/Book.Application/Extensions/AspNetUser.cs | 67 +++++ .../Extensions/CustomAuthorize.cs | 48 +++ .../Extensions/ExceptionMiddleware.cs | 32 ++ .../Extensions/IdentityMessagesPortuguese.cs | 28 ++ .../Extensions/SqlServerHealthCheck.cs | 35 +++ .../20221204190540_Identity.Designer.cs | 282 ++++++++++++++++++ .../Migrations/20221204190540_Identity.cs | 221 ++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 280 +++++++++++++++++ src/Book.Application/Program.cs | 40 +++ .../Properties/launchSettings.json | 31 ++ src/Book.Application/WeatherForecast.cs | 12 + .../appsettings.Development.json | 18 ++ src/Book.Application/appsettings.json | 18 ++ src/Book.Domain/Book.Domain.csproj | 13 + src/Book.Domain/Interfaces/IBookRepository.cs | 9 + src/Book.Domain/Interfaces/IBookService.cs | 11 + src/Book.Domain/Interfaces/INotifier.cs | 11 + src/Book.Domain/Interfaces/IRepository.cs | 16 + src/Book.Domain/Interfaces/IUser.cs | 14 + src/Book.Domain/Models/Book.cs | 38 +++ src/Book.Domain/Models/Entity.cs | 15 + src/Book.Domain/Models/User.cs | 66 ++++ .../Models/Validations/BookValidation.cs | 25 ++ .../Validations/Documents/DocsValidation.cs | 176 +++++++++++ src/Book.Domain/Notifications/Notification.cs | 12 + src/Book.Domain/Notifications/Notifier.cs | 29 ++ ...CoreApp,Version=v6.0.AssemblyAttributes.cs | 4 + .../Debug/net6.0/BackendTest.AssemblyInfo.cs | 23 ++ .../BackendTest.AssemblyInfoInputs.cache | 1 + ....GeneratedMSBuildEditorConfig.editorconfig | 16 + .../net6.0/BackendTest.GlobalUsings.g.cs | 17 ++ ...BackendTest.csproj.AssemblyReference.cache | Bin 0 -> 173738 bytes ...CoreApp,Version=v6.0.AssemblyAttributes.cs | 4 + .../net6.0/BackendTest.AssemblyInfo.cs | 23 ++ .../BackendTest.AssemblyInfoInputs.cache | 1 + ....GeneratedMSBuildEditorConfig.editorconfig | 16 + .../net6.0/BackendTest.GlobalUsings.g.cs | 17 ++ ...BackendTest.csproj.AssemblyReference.cache | Bin 0 -> 173738 bytes src/Book.Infra.Data/Book.Infra.Data.csproj | 21 ++ .../Context/BookApiDbContext.cs | 64 ++++ src/Book.Infra.Data/Mappings/BookMapping.cs | 28 ++ .../20221204190009_Initial.Designer.cs | 89 ++++++ .../Migrations/20221204190009_Initial.cs | 43 +++ .../BookApiDbContextModelSnapshot.cs | 87 ++++++ .../Repository/BookRepository.cs | 19 ++ src/Book.Infra.Data/Repository/Repository.cs | 63 ++++ src/Book.Service/Book.Service.csproj | 17 ++ src/Book.Service/Services/BaseService.cs | 42 +++ src/Book.Service/Services/BookService.cs | 40 +++ 86 files changed, 4094 insertions(+) create mode 100644 BackendTest.sln create mode 100644 src/.vscode/launch.json create mode 100644 src/.vscode/settings.json create mode 100644 src/.vscode/tasks.json create mode 100644 src/Book.Aplication/Configuration/ApiConfig.cs create mode 100644 src/Book.Aplication/Configuration/AutoMapperConfig.cs create mode 100644 src/Book.Aplication/Configuration/DependencyInjectionConfig.cs create mode 100644 src/Book.Aplication/Configuration/IdentityConfig.cs create mode 100644 src/Book.Aplication/Configuration/LoggerConfig.cs create mode 100644 src/Book.Aplication/Configuration/SwaggerConfig.cs create mode 100644 src/Book.Aplication/Controllers/AuthController.cs create mode 100644 src/Book.Aplication/Controllers/BookController.cs create mode 100644 src/Book.Aplication/Controllers/MainController.cs create mode 100644 src/Book.Aplication/Data/ApplicationDbContext.cs create mode 100644 src/Book.Aplication/Extensions/AppSettings.cs create mode 100644 src/Book.Aplication/Extensions/AspNetUser.cs create mode 100644 src/Book.Aplication/Extensions/CustomAuthorize.cs create mode 100644 src/Book.Aplication/Extensions/ExceptionMiddleware.cs create mode 100644 src/Book.Aplication/Extensions/IdentityMessagesPortuguese.cs create mode 100644 src/Book.Aplication/Extensions/SqlServerHealthCheck.cs create mode 100644 src/Book.Aplication/Program.cs create mode 100644 src/Book.Aplication/Properties/launchSettings.json create mode 100644 src/Book.Aplication/appsettings.Development.json create mode 100644 src/Book.Aplication/appsettings.json create mode 100644 src/Book.Application/Book.Application.csproj create mode 100644 src/Book.Application/Configuration/ApiConfig.cs create mode 100644 src/Book.Application/Configuration/AutoMapperConfig.cs create mode 100644 src/Book.Application/Configuration/DependencyInjectionConfig.cs create mode 100644 src/Book.Application/Configuration/IdentityConfig.cs create mode 100644 src/Book.Application/Configuration/LoggerConfig.cs create mode 100644 src/Book.Application/Configuration/SwaggerConfig.cs create mode 100644 src/Book.Application/Controllers/AuthController.cs create mode 100644 src/Book.Application/Controllers/BookController.cs create mode 100644 src/Book.Application/Controllers/MainController.cs create mode 100644 src/Book.Application/Controllers/WeatherForecastController.cs create mode 100644 src/Book.Application/Data/ApplicationDbContext.cs create mode 100644 src/Book.Application/Extensions/AppSettings.cs create mode 100644 src/Book.Application/Extensions/AspNetUser.cs create mode 100644 src/Book.Application/Extensions/CustomAuthorize.cs create mode 100644 src/Book.Application/Extensions/ExceptionMiddleware.cs create mode 100644 src/Book.Application/Extensions/IdentityMessagesPortuguese.cs create mode 100644 src/Book.Application/Extensions/SqlServerHealthCheck.cs create mode 100644 src/Book.Application/Migrations/20221204190540_Identity.Designer.cs create mode 100644 src/Book.Application/Migrations/20221204190540_Identity.cs create mode 100644 src/Book.Application/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 src/Book.Application/Program.cs create mode 100644 src/Book.Application/Properties/launchSettings.json create mode 100644 src/Book.Application/WeatherForecast.cs create mode 100644 src/Book.Application/appsettings.Development.json create mode 100644 src/Book.Application/appsettings.json create mode 100644 src/Book.Domain/Book.Domain.csproj create mode 100644 src/Book.Domain/Interfaces/IBookRepository.cs create mode 100644 src/Book.Domain/Interfaces/IBookService.cs create mode 100644 src/Book.Domain/Interfaces/INotifier.cs create mode 100644 src/Book.Domain/Interfaces/IRepository.cs create mode 100644 src/Book.Domain/Interfaces/IUser.cs create mode 100644 src/Book.Domain/Models/Book.cs create mode 100644 src/Book.Domain/Models/Entity.cs create mode 100644 src/Book.Domain/Models/User.cs create mode 100644 src/Book.Domain/Models/Validations/BookValidation.cs create mode 100644 src/Book.Domain/Models/Validations/Documents/DocsValidation.cs create mode 100644 src/Book.Domain/Notifications/Notification.cs create mode 100644 src/Book.Domain/Notifications/Notifier.cs create mode 100644 src/Book.Domain/obj/Debug/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs create mode 100644 src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfo.cs create mode 100644 src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfoInputs.cache create mode 100644 src/Book.Domain/obj/Debug/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig create mode 100644 src/Book.Domain/obj/Debug/net6.0/BackendTest.GlobalUsings.g.cs create mode 100644 src/Book.Domain/obj/Debug/net6.0/BackendTest.csproj.AssemblyReference.cache create mode 100644 src/Book.Domain/obj/Release/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs create mode 100644 src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfo.cs create mode 100644 src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfoInputs.cache create mode 100644 src/Book.Domain/obj/Release/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig create mode 100644 src/Book.Domain/obj/Release/net6.0/BackendTest.GlobalUsings.g.cs create mode 100644 src/Book.Domain/obj/Release/net6.0/BackendTest.csproj.AssemblyReference.cache create mode 100644 src/Book.Infra.Data/Book.Infra.Data.csproj create mode 100644 src/Book.Infra.Data/Context/BookApiDbContext.cs create mode 100644 src/Book.Infra.Data/Mappings/BookMapping.cs create mode 100644 src/Book.Infra.Data/Migrations/20221204190009_Initial.Designer.cs create mode 100644 src/Book.Infra.Data/Migrations/20221204190009_Initial.cs create mode 100644 src/Book.Infra.Data/Migrations/BookApiDbContextModelSnapshot.cs create mode 100644 src/Book.Infra.Data/Repository/BookRepository.cs create mode 100644 src/Book.Infra.Data/Repository/Repository.cs create mode 100644 src/Book.Service/Book.Service.csproj create mode 100644 src/Book.Service/Services/BaseService.cs create mode 100644 src/Book.Service/Services/BookService.cs diff --git a/BackendTest.sln b/BackendTest.sln new file mode 100644 index 00000000..c750c230 --- /dev/null +++ b/BackendTest.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1FA8D7EC-62D2-4671-A019-5F281786963B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Book.Application", "src\Book.Application\Book.Application.csproj", "{ACC26AFA-DFC1-4C23-98BB-142D3FAD8DAF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Book.Domain", "src\Book.Domain\Book.Domain.csproj", "{FB8FC627-58A4-4F75-AAB1-49962CB2D759}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Book.Infra.Data", "src\Book.Infra.Data\Book.Infra.Data.csproj", "{F01392B5-D9FF-43D2-8FA2-01CB923E7E98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Book.Service", "src\Book.Service\Book.Service.csproj", "{4149E5E5-3A55-4383-81A5-8D60289BF15C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ACC26AFA-DFC1-4C23-98BB-142D3FAD8DAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACC26AFA-DFC1-4C23-98BB-142D3FAD8DAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACC26AFA-DFC1-4C23-98BB-142D3FAD8DAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACC26AFA-DFC1-4C23-98BB-142D3FAD8DAF}.Release|Any CPU.Build.0 = Release|Any CPU + {FB8FC627-58A4-4F75-AAB1-49962CB2D759}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB8FC627-58A4-4F75-AAB1-49962CB2D759}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB8FC627-58A4-4F75-AAB1-49962CB2D759}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB8FC627-58A4-4F75-AAB1-49962CB2D759}.Release|Any CPU.Build.0 = Release|Any CPU + {F01392B5-D9FF-43D2-8FA2-01CB923E7E98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F01392B5-D9FF-43D2-8FA2-01CB923E7E98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F01392B5-D9FF-43D2-8FA2-01CB923E7E98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F01392B5-D9FF-43D2-8FA2-01CB923E7E98}.Release|Any CPU.Build.0 = Release|Any CPU + {4149E5E5-3A55-4383-81A5-8D60289BF15C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4149E5E5-3A55-4383-81A5-8D60289BF15C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4149E5E5-3A55-4383-81A5-8D60289BF15C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4149E5E5-3A55-4383-81A5-8D60289BF15C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {ACC26AFA-DFC1-4C23-98BB-142D3FAD8DAF} = {1FA8D7EC-62D2-4671-A019-5F281786963B} + {FB8FC627-58A4-4F75-AAB1-49962CB2D759} = {1FA8D7EC-62D2-4671-A019-5F281786963B} + {F01392B5-D9FF-43D2-8FA2-01CB923E7E98} = {1FA8D7EC-62D2-4671-A019-5F281786963B} + {4149E5E5-3A55-4383-81A5-8D60289BF15C} = {1FA8D7EC-62D2-4671-A019-5F281786963B} + EndGlobalSection +EndGlobal diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json new file mode 100644 index 00000000..dc4a6826 --- /dev/null +++ b/src/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net6.0/BackendTest.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json new file mode 100644 index 00000000..af89dd18 --- /dev/null +++ b/src/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "sqltools.connections": [] +} \ No newline at end of file diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json new file mode 100644 index 00000000..bcdb64d7 --- /dev/null +++ b/src/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/BackendTest.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/BackendTest.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/BackendTest.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/Book.Aplication/Configuration/ApiConfig.cs b/src/Book.Aplication/Configuration/ApiConfig.cs new file mode 100644 index 00000000..3754471e --- /dev/null +++ b/src/Book.Aplication/Configuration/ApiConfig.cs @@ -0,0 +1,100 @@ +using BackendTest.Extensions; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Mvc; + +namespace BackendTest.Configuration +{ + public static class ApiConfig + { + public static IServiceCollection AddApiConfig(this IServiceCollection services) + { + services.AddControllers(); + + services.AddApiVersioning(options => + { + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ReportApiVersions = true; + }); + + services.AddVersionedApiExplorer(option => + { + option.GroupNameFormat = "'v'VVV"; + option.SubstituteApiVersionInUrl = true; + }); + + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + + services.AddCors(options => + { + options.AddPolicy("Development", + builder => + builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + + options.AddPolicy("Production", + builder => + builder + .WithMethods("GET") + .WithOrigins("http://desenvolvedor.io") + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyHeader()); + }); + + return services; + } + + public static IApplicationBuilder UseApiConfig(this IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseCors("Development"); + app.UseDeveloperExceptionPage(); + } + else + { + app.UseCors("Development"); + app.UseHsts(); + } + + app.UseMiddleware(); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseStaticFiles(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/api/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecksUI(options => + { + options.UIPath = "/api/hc-ui"; + options.ResourcesPath = "/api/hc-ui-resources"; + + options.UseRelativeApiPath = false; + options.UseRelativeResourcesPath = false; + options.UseRelativeWebhookPath = false; + }); + + }); + + return app; + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Configuration/AutoMapperConfig.cs b/src/Book.Aplication/Configuration/AutoMapperConfig.cs new file mode 100644 index 00000000..9661636a --- /dev/null +++ b/src/Book.Aplication/Configuration/AutoMapperConfig.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using BackendTest.Models; + +namespace BackendTest.Configuration +{ + public class AutoMapperConfig : Profile + { + public AutoMapperConfig() + { + CreateMap(); + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Configuration/DependencyInjectionConfig.cs b/src/Book.Aplication/Configuration/DependencyInjectionConfig.cs new file mode 100644 index 00000000..c68a82e7 --- /dev/null +++ b/src/Book.Aplication/Configuration/DependencyInjectionConfig.cs @@ -0,0 +1,30 @@ +using BackendTest.Context; +using BackendTest.Extensions; +using BackendTest.Interfaces; +using BackendTest.Notifications; +using BackendTest.Repository; +using BackendTest.Services; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace BackendTest.Configuration +{ + public static class DependencyInjectionConfig + { + public static IServiceCollection ResolveDependencies(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(); + services.AddScoped(); + + services.AddTransient, ConfigureSwaggerOptions>(); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Configuration/IdentityConfig.cs b/src/Book.Aplication/Configuration/IdentityConfig.cs new file mode 100644 index 00000000..8cde00d6 --- /dev/null +++ b/src/Book.Aplication/Configuration/IdentityConfig.cs @@ -0,0 +1,53 @@ +using System.Text; +using BackendTest.Extensions; +using BackendTest.Data; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; + +namespace BackendTest.Configuration +{ + public static class IdentityConfig + { + public static IServiceCollection AddIdentityConfig(this IServiceCollection services, + IConfiguration configuration) + { + services.AddDbContext(); + + services.AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores() + .AddErrorDescriber() + .AddDefaultTokenProviders(); + + // JWT + + var appSettingsSection = configuration.GetSection("AppSettings"); + services.Configure(appSettingsSection); + + var appSettings = appSettingsSection.Get(); + var key = Encoding.ASCII.GetBytes(appSettings.Secret); + + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(x => + { + x.RequireHttpsMetadata = true; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = appSettings.ValidoEm, + ValidIssuer = appSettings.Emissor + }; + }); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Configuration/LoggerConfig.cs b/src/Book.Aplication/Configuration/LoggerConfig.cs new file mode 100644 index 00000000..d50a1e3f --- /dev/null +++ b/src/Book.Aplication/Configuration/LoggerConfig.cs @@ -0,0 +1,39 @@ +using BackendTest.Extensions; + +namespace BackendTest.Configuration +{ + public static class LoggerConfig + { + public static IServiceCollection AddLoggingConfig(this IServiceCollection services, IConfiguration configuration) + { + services.AddElmahIo(o => + { + o.ApiKey = "388dd3a277cb44c4aa128b5c899a3106"; + o.LogId = new Guid("c468b2b8-b35d-4f1a-849d-f47b60eef096"); + }); + + services.AddHealthChecks() + .AddElmahIoPublisher(options => + { + options.ApiKey = "388dd3a277cb44c4aa128b5c899a3106"; + options.LogId = new Guid("c468b2b8-b35d-4f1a-849d-f47b60eef096"); + options.HeartbeatId = "API Fornecedores"; + + }) + .AddCheck("Produtos", new SqlServerHealthCheck(configuration.GetConnectionString("DefaultConnection"))) + .AddSqlServer(configuration.GetConnectionString("DefaultConnection"), name: "BancoSQL"); + + services.AddHealthChecksUI() + .AddSqlServerStorage(configuration.GetConnectionString("DefaultConnection")); + + return services; + } + + public static IApplicationBuilder UseLoggingConfiguration(this IApplicationBuilder app) + { + app.UseElmahIo(); + + return app; + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Configuration/SwaggerConfig.cs b/src/Book.Aplication/Configuration/SwaggerConfig.cs new file mode 100644 index 00000000..b9050379 --- /dev/null +++ b/src/Book.Aplication/Configuration/SwaggerConfig.cs @@ -0,0 +1,153 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace BackendTest.Configuration +{ + public static class SwaggerConfig + { + public static IServiceCollection AddSwaggerConfig(this IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.OperationFilter(); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "Insira o token JWT desta maneira: Bearer {seu token}", + Name = "Authorization", + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); + }); + + return services; + } + + public static IApplicationBuilder UseSwaggerConfig(this IApplicationBuilder app, IApiVersionDescriptionProvider provider) + { + //app.UseMiddleware(); + app.UseSwagger(); + app.UseSwaggerUI( + options => + { + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + } + }); + return app; + } + } + + public class ConfigureSwaggerOptions : IConfigureOptions + { + readonly IApiVersionDescriptionProvider provider; + + public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider; + + public void Configure(SwaggerGenOptions options) + { + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); + } + } + + static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + { + var info = new OpenApiInfo() + { + Title = "Book API - Backend Test", + Version = description.ApiVersion.ToString(), + Description = "Esta API foi feita para o Desafio Backend.", + }; + + if (description.IsDeprecated) + { + info.Description += " Esta versão está obsoleta!"; + } + + return info; + } + } + + public class SwaggerDefaultValues : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var apiDescription = context.ApiDescription; + + operation.Deprecated |= apiDescription.IsDeprecated(); + + foreach (var responseType in context.ApiDescription.SupportedResponseTypes) + { + var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(); + var response = operation.Responses[responseKey]; + + foreach (var contentType in response.Content.Keys) + if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType)) + response.Content.Remove(contentType); + } + + if (operation.Parameters == null) + return; + + foreach (var parameter in operation.Parameters) + { + var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); + + parameter.Description ??= description.ModelMetadata.Description; + + if (parameter.Schema.Default == null && description.DefaultValue != null) + { + var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType); + parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json); + } + + parameter.Required |= description.IsRequired; + } + } + } + + public class SwaggerAuthorizedMiddleware + { + private readonly RequestDelegate _next; + + public SwaggerAuthorizedMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/swagger") + && !context.User.Identity.IsAuthenticated) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } + + await _next.Invoke(context); + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Controllers/AuthController.cs b/src/Book.Aplication/Controllers/AuthController.cs new file mode 100644 index 00000000..cccfaff3 --- /dev/null +++ b/src/Book.Aplication/Controllers/AuthController.cs @@ -0,0 +1,133 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using BackendTest.Extensions; +using BackendTest.Interfaces; +using BackendTest.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace BackendTest.Controllers; + +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}")] +public class AuthController : MainController +{ + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly AppSettings _appSettings; + private readonly ILogger _logger; + + public AuthController(INotifier notifier, + SignInManager signInManager, + UserManager userManager, + IOptions appSettings, + IUser user, ILogger logger) : base(notifier, user) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + _appSettings = appSettings.Value; + } + + //[EnableCors("Development")] + [HttpPost("nova-conta")] + public async Task Registrar(RegisterUser registerUser) + { + if (!ModelState.IsValid) return CustomResponse(ModelState); + + var user = new IdentityUser + { + UserName = registerUser.Email, + Email = registerUser.Email, + EmailConfirmed = true + }; + + var result = await _userManager.CreateAsync(user, registerUser.Password); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, false); + return CustomResponse(await GerarJwt(user.Email)); + } + foreach (var error in result.Errors) + { + NotificarErro(error.Description); + } + + return CustomResponse(registerUser); + } + + [HttpPost("entrar")] + public async Task Login(LoginUser loginUser) + { + if (!ModelState.IsValid) return CustomResponse(ModelState); + + var result = await _signInManager.PasswordSignInAsync(loginUser.Email, loginUser.Password, false, true); + + if (result.Succeeded) + { + _logger.LogInformation("Usuario " + loginUser.Email + " logado com sucesso"); + return CustomResponse(await GerarJwt(loginUser.Email)); + } + if (result.IsLockedOut) + { + NotificarErro("Usuário temporariamente bloqueado por tentativas inválidas"); + return CustomResponse(loginUser); + } + + NotificarErro("Usuário ou Senha incorretos"); + return CustomResponse(loginUser); + } + + private async Task GerarJwt(string email) + { + var user = await _userManager.FindByEmailAsync(email); + var claims = await _userManager.GetClaimsAsync(user); + var userRoles = await _userManager.GetRolesAsync(user); + + claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.Id)); + claims.Add(new Claim(JwtRegisteredClaimNames.Email, user.Email)); + claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())); + claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, ToUnixEpochDate(DateTime.UtcNow).ToString())); + claims.Add(new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(DateTime.UtcNow).ToString(), ClaimValueTypes.Integer64)); + foreach (var userRole in userRoles) + { + claims.Add(new Claim("role", userRole)); + } + + var identityClaims = new ClaimsIdentity(); + identityClaims.AddClaims(claims); + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_appSettings.Secret); + var token = tokenHandler.CreateToken(new SecurityTokenDescriptor + { + Issuer = _appSettings.Emissor, + Audience = _appSettings.ValidoEm, + Subject = identityClaims, + Expires = DateTime.UtcNow.AddHours(_appSettings.ExpiracaoHoras), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }); + + var encodedToken = tokenHandler.WriteToken(token); + + var response = new LoginResponseModel + { + AccessToken = encodedToken, + ExpiresIn = TimeSpan.FromHours(_appSettings.ExpiracaoHoras).TotalSeconds, + UserToken = new UserToken + { + Id = user.Id, + Email = user.Email, + Claims = claims.Select(c => new ClaimModel { Type = c.Type, Value = c.Value }) + } + }; + + return response; + } + + private static long ToUnixEpochDate(DateTime date) + => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds); +} diff --git a/src/Book.Aplication/Controllers/BookController.cs b/src/Book.Aplication/Controllers/BookController.cs new file mode 100644 index 00000000..9acc5d01 --- /dev/null +++ b/src/Book.Aplication/Controllers/BookController.cs @@ -0,0 +1,80 @@ +using BackendTest.Context; +using BackendTest.Extensions; +using BackendTest.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace BackendTest.Controllers; + +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/book")] +public class BookController : ControllerBase +{ + private readonly BookApiDbContext _context; + + public BookController(BookApiDbContext context) => + _context = context; + + [AllowAnonymous] + [HttpGet] + public async Task> Get() => + await _context.Books.ToListAsync(); + + [AllowAnonymous] + [HttpGet("{id}")] + public async Task> Get(string id) + { + var book = await _context.Books.FindAsync(id); + + if (book is null) + { + return NotFound(); + } + + return book; + } + + [HttpPost] + public async Task Create([FromBody] Book book) + { + _context.Books.Add(book); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(Get), new { id = book.Id }, book); + } + + [HttpPut("{id}")] + public async Task Update(string id, [FromBody] Book book) + { + await _context.Books.FindAsync(id); + + if (book is null) + { + return NotFound(); + } + + _context.Books.Update(book); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(string id) + { + var book = await _context.Books.FindAsync(id); + + if (book is null) + { + return NotFound(); + } + + _context.Books.Remove(book); + await _context.SaveChangesAsync(); + + return NoContent(); + } +} diff --git a/src/Book.Aplication/Controllers/MainController.cs b/src/Book.Aplication/Controllers/MainController.cs new file mode 100644 index 00000000..9f74985d --- /dev/null +++ b/src/Book.Aplication/Controllers/MainController.cs @@ -0,0 +1,73 @@ +using BackendTest.Interfaces; +using BackendTest.Notifications; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace BackendTest.Controllers; + +[ApiController] +public class MainController : ControllerBase +{ + private readonly INotifier _notifier; + public readonly IUser AppUser; + + protected Guid UserId { get; set; } + protected bool UserAuthenticated { get; set; } + + protected MainController(INotifier notifier, + IUser appUser) + { + _notifier = notifier; + AppUser = appUser; + + if (appUser.IsAuthenticated()) + { + UserId = appUser.GetUserId(); + UserAuthenticated = true; + } + } + + protected bool ValidOperation() + { + return !_notifier.HasNotification(); + } + + protected ActionResult CustomResponse(object result = null) + { + if (ValidOperation()) + { + return Ok(new + { + success = true, + data = result + }); + } + + return BadRequest(new + { + success = false, + errors = _notifier.GetNotifications().Select(n => n.Message) + }); + } + + protected ActionResult CustomResponse(ModelStateDictionary modelState) + { + if (!modelState.IsValid) NotifyErrorModelInvalid(modelState); + return CustomResponse(); + } + + protected void NotifyErrorModelInvalid(ModelStateDictionary modelState) + { + var erros = modelState.Values.SelectMany(e => e.Errors); + foreach (var erro in erros) + { + var errorMsg = erro.Exception == null ? erro.ErrorMessage : erro.Exception.Message; + NotificarErro(errorMsg); + } + } + + protected void NotificarErro(string message) + { + _notifier.Handle(new Notification(message)); + } +} diff --git a/src/Book.Aplication/Data/ApplicationDbContext.cs b/src/Book.Aplication/Data/ApplicationDbContext.cs new file mode 100644 index 00000000..c8804266 --- /dev/null +++ b/src/Book.Aplication/Data/ApplicationDbContext.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace BackendTest.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) : base(options) { } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + => options.UseSqlServer("Server=localhost,1433;Database=BackendTest;User Id=sa;Password=123456Sa;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=true"); + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Extensions/AppSettings.cs b/src/Book.Aplication/Extensions/AppSettings.cs new file mode 100644 index 00000000..ce7e767a --- /dev/null +++ b/src/Book.Aplication/Extensions/AppSettings.cs @@ -0,0 +1,10 @@ +namespace BackendTest.Extensions +{ + public class AppSettings + { + public string Secret { get; set; } + public int ExpiracaoHoras { get; set; } + public string Emissor { get; set; } + public string ValidoEm { get; set; } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Extensions/AspNetUser.cs b/src/Book.Aplication/Extensions/AspNetUser.cs new file mode 100644 index 00000000..09ea7e55 --- /dev/null +++ b/src/Book.Aplication/Extensions/AspNetUser.cs @@ -0,0 +1,67 @@ +using System.Security.Claims; +using BackendTest.Interfaces; + +namespace BackendTest.Extensions +{ + public class AspNetUser : IUser + { + private readonly IHttpContextAccessor _accessor; + + public AspNetUser(IHttpContextAccessor accessor) + { + _accessor = accessor; + } + + public string Name => _accessor.HttpContext.User.Identity.Name; + + public Guid GetUserId() + { + return IsAuthenticated() ? Guid.Parse(_accessor.HttpContext.User.GetUserId()) : Guid.Empty; + } + + public string GetUserEmail() + { + return IsAuthenticated() ? _accessor.HttpContext.User.GetUserEmail() : ""; + } + + public bool IsAuthenticated() + { + return _accessor.HttpContext.User.Identity.IsAuthenticated; + } + + public bool IsInRole(string role) + { + return _accessor.HttpContext.User.IsInRole(role); + } + + public IEnumerable GetClaimsIdentity() + { + return _accessor.HttpContext.User.Claims; + } + } + + public static class ClaimsPrincipalExtensions + { + public static string GetUserId(this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + + var claim = principal.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + + public static string GetUserEmail(this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + + var claim = principal.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Extensions/CustomAuthorize.cs b/src/Book.Aplication/Extensions/CustomAuthorize.cs new file mode 100644 index 00000000..4dc0bd23 --- /dev/null +++ b/src/Book.Aplication/Extensions/CustomAuthorize.cs @@ -0,0 +1,48 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace BackendTest.Extensions +{ + public class CustomAuthorization + { + public static bool ValidarClaimsUsuario(HttpContext context, string claimName, string claimValue) + { + return context.User.Identity.IsAuthenticated && + context.User.Claims.Any(c => c.Type == claimName && c.Value.Contains(claimValue)); + } + + } + + public class ClaimsAuthorizeAttribute : TypeFilterAttribute + { + public ClaimsAuthorizeAttribute(string claimName, string claimValue) : base(typeof(RequisitoClaimFilter)) + { + Arguments = new object[] { new Claim(claimName, claimValue) }; + } + } + + public class RequisitoClaimFilter : IAuthorizationFilter + { + private readonly Claim _claim; + + public RequisitoClaimFilter(Claim claim) + { + _claim = claim; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext.User.Identity.IsAuthenticated) + { + context.Result = new StatusCodeResult(401); + return; + } + + if (!CustomAuthorization.ValidarClaimsUsuario(context.HttpContext, _claim.Type, _claim.Value)) + { + context.Result = new StatusCodeResult(403); + } + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Extensions/ExceptionMiddleware.cs b/src/Book.Aplication/Extensions/ExceptionMiddleware.cs new file mode 100644 index 00000000..106c4ece --- /dev/null +++ b/src/Book.Aplication/Extensions/ExceptionMiddleware.cs @@ -0,0 +1,32 @@ +using System.Net; + +namespace BackendTest.Extensions +{ + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + + public ExceptionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext httpContext) + { + try + { + await _next(httpContext); + } + catch (Exception ex) + { + HandleExceptionAsync(httpContext, ex); + } + } + + private static void HandleExceptionAsync(HttpContext context, Exception exception) + { + //exception.Ship(context); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Extensions/IdentityMessagesPortuguese.cs b/src/Book.Aplication/Extensions/IdentityMessagesPortuguese.cs new file mode 100644 index 00000000..609251e9 --- /dev/null +++ b/src/Book.Aplication/Extensions/IdentityMessagesPortuguese.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Identity; + +namespace BackendTest.Extensions +{ + public class IdentityMessagesPortuguese : IdentityErrorDescriber + { + public override IdentityError DefaultError() { return new IdentityError { Code = nameof(DefaultError), Description = $"Ocorreu um erro desconhecido." }; } + public override IdentityError ConcurrencyFailure() { return new IdentityError { Code = nameof(ConcurrencyFailure), Description = "Falha de concorrência otimista, o objeto foi modificado." }; } + public override IdentityError PasswordMismatch() { return new IdentityError { Code = nameof(PasswordMismatch), Description = "Senha incorreta." }; } + public override IdentityError InvalidToken() { return new IdentityError { Code = nameof(InvalidToken), Description = "Token inválido." }; } + public override IdentityError LoginAlreadyAssociated() { return new IdentityError { Code = nameof(LoginAlreadyAssociated), Description = "Já existe um usuário com este login." }; } + public override IdentityError InvalidUserName(string userName) { return new IdentityError { Code = nameof(InvalidUserName), Description = $"Login '{userName}' é inválido, pode conter apenas letras ou dígitos." }; } + public override IdentityError InvalidEmail(string email) { return new IdentityError { Code = nameof(InvalidEmail), Description = $"Email '{email}' é inválido." }; } + public override IdentityError DuplicateUserName(string userName) { return new IdentityError { Code = nameof(DuplicateUserName), Description = $"Login '{userName}' já está sendo utilizado." }; } + public override IdentityError DuplicateEmail(string email) { return new IdentityError { Code = nameof(DuplicateEmail), Description = $"Email '{email}' já está sendo utilizado." }; } + public override IdentityError InvalidRoleName(string role) { return new IdentityError { Code = nameof(InvalidRoleName), Description = $"A permissão '{role}' é inválida." }; } + public override IdentityError DuplicateRoleName(string role) { return new IdentityError { Code = nameof(DuplicateRoleName), Description = $"A permissão '{role}' já está sendo utilizada." }; } + public override IdentityError UserAlreadyHasPassword() { return new IdentityError { Code = nameof(UserAlreadyHasPassword), Description = "Usuário já possui uma senha definida." }; } + public override IdentityError UserLockoutNotEnabled() { return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = "Lockout não está habilitado para este usuário." }; } + public override IdentityError UserAlreadyInRole(string role) { return new IdentityError { Code = nameof(UserAlreadyInRole), Description = $"Usuário já possui a permissão '{role}'." }; } + public override IdentityError UserNotInRole(string role) { return new IdentityError { Code = nameof(UserNotInRole), Description = $"Usuário não tem a permissão '{role}'." }; } + public override IdentityError PasswordTooShort(int length) { return new IdentityError { Code = nameof(PasswordTooShort), Description = $"Senhas devem conter ao menos {length} caracteres." }; } + public override IdentityError PasswordRequiresNonAlphanumeric() { return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = "Senhas devem conter ao menos um caracter não alfanumérico." }; } + public override IdentityError PasswordRequiresDigit() { return new IdentityError { Code = nameof(PasswordRequiresDigit), Description = "Senhas devem conter ao menos um digito ('0'-'9')." }; } + public override IdentityError PasswordRequiresLower() { return new IdentityError { Code = nameof(PasswordRequiresLower), Description = "Senhas devem conter ao menos um caracter em caixa baixa ('a'-'z')." }; } + public override IdentityError PasswordRequiresUpper() { return new IdentityError { Code = nameof(PasswordRequiresUpper), Description = "Senhas devem conter ao menos um caracter em caixa alta ('A'-'Z')." }; } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Extensions/SqlServerHealthCheck.cs b/src/Book.Aplication/Extensions/SqlServerHealthCheck.cs new file mode 100644 index 00000000..5e15fa73 --- /dev/null +++ b/src/Book.Aplication/Extensions/SqlServerHealthCheck.cs @@ -0,0 +1,35 @@ +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace BackendTest.Extensions +{ + public class SqlServerHealthCheck : IHealthCheck + { + readonly string _connection; + + public SqlServerHealthCheck(string connection) + { + _connection = connection; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) + { + try + { + using (var connection = new SqlConnection(_connection)) + { + await connection.OpenAsync(cancellationToken); + + var command = connection.CreateCommand(); + command.CommandText = "select count(id) from books"; + + return Convert.ToInt32(await command.ExecuteScalarAsync(cancellationToken)) > 0 ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy(); + } + } + catch (Exception) + { + return HealthCheckResult.Unhealthy(); + } + } + } +} \ No newline at end of file diff --git a/src/Book.Aplication/Program.cs b/src/Book.Aplication/Program.cs new file mode 100644 index 00000000..e2350570 --- /dev/null +++ b/src/Book.Aplication/Program.cs @@ -0,0 +1,40 @@ +using BackendTest.Context; +using BackendTest.Configuration; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) + .AddEnvironmentVariables(); + +// ConfigureServices + +builder.Services.AddDbContext(); + +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + +builder.Services.AddIdentityConfig(builder.Configuration); + +builder.Services.AddApiConfig(); + +builder.Services.AddSwaggerConfig(); + +builder.Services.AddLoggingConfig(builder.Configuration); + +builder.Services.ResolveDependencies(); + +var app = builder.Build(); +var apiVersionDescriptionProvider = app.Services.GetRequiredService(); + +// Configure + +app.UseApiConfig(app.Environment); + +app.UseSwaggerConfig(apiVersionDescriptionProvider); + +app.UseLoggingConfiguration(); + +app.Run(); diff --git a/src/Book.Aplication/Properties/launchSettings.json b/src/Book.Aplication/Properties/launchSettings.json new file mode 100644 index 00000000..9bc9ac62 --- /dev/null +++ b/src/Book.Aplication/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:11294", + "sslPort": 44397 + } + }, + "profiles": { + "BackendTest": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7010;http://localhost:5113", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Book.Aplication/appsettings.Development.json b/src/Book.Aplication/appsettings.Development.json new file mode 100644 index 00000000..79c88420 --- /dev/null +++ b/src/Book.Aplication/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost,1433;Database=BackendTest;User Id=sa;Password=123456Sa;Trusted_Connection=False;TrustServerCertificate=True" + }, + "AppSettings": { + "Secret": "SUPERSECRETAUTHENTICATIONKEY", + "ExpiracaoHoras": 2, + "Emissor": "Sistema", + "ValidoEm": "https://localhost" + } +} \ No newline at end of file diff --git a/src/Book.Aplication/appsettings.json b/src/Book.Aplication/appsettings.json new file mode 100644 index 00000000..f79e4d93 --- /dev/null +++ b/src/Book.Aplication/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost,1433;Database=BackendTest;User Id=sa;Password=123456Sa;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + + "AppSettings": { + "Secret": "SUPERSECRETAUTHENTICATIONKEY", + "ExpiracaoHoras": 2, + "Emissor": "Sistema", + "ValidoEm": "https://localhost" + } +} diff --git a/src/Book.Application/Book.Application.csproj b/src/Book.Application/Book.Application.csproj new file mode 100644 index 00000000..70a18d7e --- /dev/null +++ b/src/Book.Application/Book.Application.csproj @@ -0,0 +1,43 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/src/Book.Application/Configuration/ApiConfig.cs b/src/Book.Application/Configuration/ApiConfig.cs new file mode 100644 index 00000000..0201053a --- /dev/null +++ b/src/Book.Application/Configuration/ApiConfig.cs @@ -0,0 +1,100 @@ +using Book.Application.Extensions; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Mvc; + +namespace Book.Application.Configuration +{ + public static class ApiConfig + { + public static IServiceCollection AddApiConfig(this IServiceCollection services) + { + services.AddControllers(); + + services.AddApiVersioning(options => + { + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ReportApiVersions = true; + }); + + services.AddVersionedApiExplorer(option => + { + option.GroupNameFormat = "'v'VVV"; + option.SubstituteApiVersionInUrl = true; + }); + + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + + services.AddCors(options => + { + options.AddPolicy("Development", + builder => + builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + + options.AddPolicy("Production", + builder => + builder + .WithMethods("GET") + .WithOrigins("http://desenvolvedor.io") + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyHeader()); + }); + + return services; + } + + public static IApplicationBuilder UseApiConfig(this IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseCors("Development"); + app.UseDeveloperExceptionPage(); + } + else + { + app.UseCors("Development"); + app.UseHsts(); + } + + app.UseMiddleware(); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseStaticFiles(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/api/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecksUI(options => + { + options.UIPath = "/api/hc-ui"; + options.ResourcesPath = "/api/hc-ui-resources"; + + options.UseRelativeApiPath = false; + options.UseRelativeResourcesPath = false; + options.UseRelativeWebhookPath = false; + }); + + }); + + return app; + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Configuration/AutoMapperConfig.cs b/src/Book.Application/Configuration/AutoMapperConfig.cs new file mode 100644 index 00000000..f4e6ac50 --- /dev/null +++ b/src/Book.Application/Configuration/AutoMapperConfig.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using Book.Domain.Models; + +namespace Book.Application.Configuration +{ + public class AutoMapperConfig : Profile + { + public AutoMapperConfig() + { + CreateMap(); + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Configuration/DependencyInjectionConfig.cs b/src/Book.Application/Configuration/DependencyInjectionConfig.cs new file mode 100644 index 00000000..d6893fe0 --- /dev/null +++ b/src/Book.Application/Configuration/DependencyInjectionConfig.cs @@ -0,0 +1,30 @@ +using Book.Application.Extensions; +using Book.Domain.Interfaces; +using Book.Domain.Notifications; +using Book.Infra.Data.Context; +using Book.Infra.Data.Repository; +using Book.Service.Services; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Book.Application.Configuration +{ + public static class DependencyInjectionConfig + { + public static IServiceCollection ResolveDependencies(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(); + services.AddScoped(); + + services.AddTransient, ConfigureSwaggerOptions>(); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Configuration/IdentityConfig.cs b/src/Book.Application/Configuration/IdentityConfig.cs new file mode 100644 index 00000000..c8eebf67 --- /dev/null +++ b/src/Book.Application/Configuration/IdentityConfig.cs @@ -0,0 +1,53 @@ +using System.Text; +using Book.Application.Data; +using Book.Application.Extensions; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; + +namespace Book.Application.Configuration +{ + public static class IdentityConfig + { + public static IServiceCollection AddIdentityConfig(this IServiceCollection services, + IConfiguration configuration) + { + services.AddDbContext(); + + services.AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores() + .AddErrorDescriber() + .AddDefaultTokenProviders(); + + // JWT + + var appSettingsSection = configuration.GetSection("AppSettings"); + services.Configure(appSettingsSection); + + var appSettings = appSettingsSection.Get(); + var key = Encoding.ASCII.GetBytes(appSettings.Secret); + + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(x => + { + x.RequireHttpsMetadata = true; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = appSettings.ValidoEm, + ValidIssuer = appSettings.Emissor + }; + }); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Configuration/LoggerConfig.cs b/src/Book.Application/Configuration/LoggerConfig.cs new file mode 100644 index 00000000..6b0caa2a --- /dev/null +++ b/src/Book.Application/Configuration/LoggerConfig.cs @@ -0,0 +1,39 @@ +using Book.Application.Extensions; + +namespace Book.Application.Configuration +{ + public static class LoggerConfig + { + public static IServiceCollection AddLoggingConfig(this IServiceCollection services, IConfiguration configuration) + { + services.AddElmahIo(o => + { + o.ApiKey = "388dd3a277cb44c4aa128b5c899a3106"; + o.LogId = new Guid("c468b2b8-b35d-4f1a-849d-f47b60eef096"); + }); + + services.AddHealthChecks() + .AddElmahIoPublisher(options => + { + options.ApiKey = "388dd3a277cb44c4aa128b5c899a3106"; + options.LogId = new Guid("c468b2b8-b35d-4f1a-849d-f47b60eef096"); + options.HeartbeatId = "API Books"; + + }) + .AddCheck("Books", new SqlServerHealthCheck(configuration.GetConnectionString("DefaultConnection"))) + .AddSqlServer(configuration.GetConnectionString("DefaultConnection"), name: "BackendTest"); + + services.AddHealthChecksUI() + .AddSqlServerStorage(configuration.GetConnectionString("DefaultConnection")); + + return services; + } + + public static IApplicationBuilder UseLoggingConfiguration(this IApplicationBuilder app) + { + app.UseElmahIo(); + + return app; + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Configuration/SwaggerConfig.cs b/src/Book.Application/Configuration/SwaggerConfig.cs new file mode 100644 index 00000000..7eb86008 --- /dev/null +++ b/src/Book.Application/Configuration/SwaggerConfig.cs @@ -0,0 +1,152 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Book.Application.Configuration +{ + public static class SwaggerConfig + { + public static IServiceCollection AddSwaggerConfig(this IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.OperationFilter(); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "Insira o token JWT desta maneira: Bearer {seu token}", + Name = "Authorization", + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); + }); + + return services; + } + + public static IApplicationBuilder UseSwaggerConfig(this IApplicationBuilder app, IApiVersionDescriptionProvider provider) + { + //app.UseMiddleware(); + app.UseSwagger(); + app.UseSwaggerUI( + options => + { + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + } + }); + return app; + } + } + public class ConfigureSwaggerOptions : IConfigureOptions + { + readonly IApiVersionDescriptionProvider provider; + + public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider; + + public void Configure(SwaggerGenOptions options) + { + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); + } + } + + static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + { + var info = new OpenApiInfo() + { + Title = "Book API - Backend Test", + Version = description.ApiVersion.ToString(), + Description = "Esta API foi feita para o Desafio Backend.", + }; + + if (description.IsDeprecated) + { + info.Description += " Esta versão está obsoleta!"; + } + + return info; + } + } + + public class SwaggerDefaultValues : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var apiDescription = context.ApiDescription; + + operation.Deprecated |= apiDescription.IsDeprecated(); + + foreach (var responseType in context.ApiDescription.SupportedResponseTypes) + { + var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(); + var response = operation.Responses[responseKey]; + + foreach (var contentType in response.Content.Keys) + if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType)) + response.Content.Remove(contentType); + } + + if (operation.Parameters == null) + return; + + foreach (var parameter in operation.Parameters) + { + var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); + + parameter.Description ??= description.ModelMetadata.Description; + + if (parameter.Schema.Default == null && description.DefaultValue != null) + { + var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType); + parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json); + } + + parameter.Required |= description.IsRequired; + } + } + } + + public class SwaggerAuthorizedMiddleware + { + private readonly RequestDelegate _next; + + public SwaggerAuthorizedMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/swagger") + && !context.User.Identity.IsAuthenticated) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } + + await _next.Invoke(context); + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Controllers/AuthController.cs b/src/Book.Application/Controllers/AuthController.cs new file mode 100644 index 00000000..5fe8c2d8 --- /dev/null +++ b/src/Book.Application/Controllers/AuthController.cs @@ -0,0 +1,133 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Book.Application.Extensions; +using Book.Domain.Interfaces; +using Book.Domain.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Book.Application.Controllers; + +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}")] +public class AuthController : MainController +{ + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly AppSettings _appSettings; + private readonly ILogger _logger; + + public AuthController(INotifier notifier, + SignInManager signInManager, + UserManager userManager, + IOptions appSettings, + IUser user, ILogger logger) : base(notifier, user) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + _appSettings = appSettings.Value; + } + + //[EnableCors("Development")] + [HttpPost("nova-conta")] + public async Task Registrar(RegisterUser registerUser) + { + if (!ModelState.IsValid) return CustomResponse(ModelState); + + var user = new IdentityUser + { + UserName = registerUser.Email, + Email = registerUser.Email, + EmailConfirmed = true + }; + + var result = await _userManager.CreateAsync(user, registerUser.Password); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, false); + return CustomResponse(await GerarJwt(user.Email)); + } + foreach (var error in result.Errors) + { + NotificarErro(error.Description); + } + + return CustomResponse(registerUser); + } + + [HttpPost("entrar")] + public async Task Login(LoginUser loginUser) + { + if (!ModelState.IsValid) return CustomResponse(ModelState); + + var result = await _signInManager.PasswordSignInAsync(loginUser.Email, loginUser.Password, false, true); + + if (result.Succeeded) + { + _logger.LogInformation("Usuario " + loginUser.Email + " logado com sucesso"); + return CustomResponse(await GerarJwt(loginUser.Email)); + } + if (result.IsLockedOut) + { + NotificarErro("Usuário temporariamente bloqueado por tentativas inválidas"); + return CustomResponse(loginUser); + } + + NotificarErro("Usuário ou Senha incorretos"); + return CustomResponse(loginUser); + } + + private async Task GerarJwt(string email) + { + var user = await _userManager.FindByEmailAsync(email); + var claims = await _userManager.GetClaimsAsync(user); + var userRoles = await _userManager.GetRolesAsync(user); + + claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.Id)); + claims.Add(new Claim(JwtRegisteredClaimNames.Email, user.Email)); + claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())); + claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, ToUnixEpochDate(DateTime.UtcNow).ToString())); + claims.Add(new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(DateTime.UtcNow).ToString(), ClaimValueTypes.Integer64)); + foreach (var userRole in userRoles) + { + claims.Add(new Claim("role", userRole)); + } + + var identityClaims = new ClaimsIdentity(); + identityClaims.AddClaims(claims); + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_appSettings.Secret); + var token = tokenHandler.CreateToken(new SecurityTokenDescriptor + { + Issuer = _appSettings.Emissor, + Audience = _appSettings.ValidoEm, + Subject = identityClaims, + Expires = DateTime.UtcNow.AddHours(_appSettings.ExpiracaoHoras), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }); + + var encodedToken = tokenHandler.WriteToken(token); + + var response = new LoginResponseModel + { + AccessToken = encodedToken, + ExpiresIn = TimeSpan.FromHours(_appSettings.ExpiracaoHoras).TotalSeconds, + UserToken = new UserToken + { + Id = user.Id, + Email = user.Email, + Claims = claims.Select(c => new ClaimModel { Type = c.Type, Value = c.Value }) + } + }; + + return response; + } + + private static long ToUnixEpochDate(DateTime date) + => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds); +} \ No newline at end of file diff --git a/src/Book.Application/Controllers/BookController.cs b/src/Book.Application/Controllers/BookController.cs new file mode 100644 index 00000000..de8b5392 --- /dev/null +++ b/src/Book.Application/Controllers/BookController.cs @@ -0,0 +1,79 @@ +using Book.Domain.Models; +using Book.Infra.Data.Context; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Book.Application.Controllers; + +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/book")] +public class BookController : ControllerBase +{ + private readonly BookApiDbContext _context; + + public BookController(BookApiDbContext context) => + _context = context; + + [AllowAnonymous] + [HttpGet] + public async Task> Get() => + await _context.Books.ToListAsync(); + + [AllowAnonymous] + [HttpGet("{id}")] + public async Task> Get(string id) + { + var book = await _context.Books.FindAsync(id); + + if (book is null) + { + return NotFound(); + } + + return book; + } + + [HttpPost] + public async Task Create([FromBody] BookModel book) + { + _context.Books.Add(book); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(Get), new { id = book.Id }, book); + } + + [HttpPut("{id}")] + public async Task Update(string id, [FromBody] BookModel book) + { + await _context.Books.FindAsync(id); + + if (book is null) + { + return NotFound(); + } + + _context.Books.Update(book); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(string id) + { + var book = await _context.Books.FindAsync(id); + + if (book is null) + { + return NotFound(); + } + + _context.Books.Remove(book); + await _context.SaveChangesAsync(); + + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Book.Application/Controllers/MainController.cs b/src/Book.Application/Controllers/MainController.cs new file mode 100644 index 00000000..a3ae3fb8 --- /dev/null +++ b/src/Book.Application/Controllers/MainController.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Book.Domain.Interfaces; +using Book.Domain.Notifications; + +namespace Book.Application.Controllers; + +[ApiController] +public class MainController : ControllerBase +{ + private readonly INotifier _notifier; + public readonly IUser AppUser; + + protected Guid UserId { get; set; } + protected bool UserAuthenticated { get; set; } + + protected MainController(INotifier notifier, + IUser appUser) + { + _notifier = notifier; + AppUser = appUser; + + if (appUser.IsAuthenticated()) + { + UserId = appUser.GetUserId(); + UserAuthenticated = true; + } + } + + protected bool ValidOperation() + { + return !_notifier.HasNotification(); + } + + protected ActionResult CustomResponse(object result = null) + { + if (ValidOperation()) + { + return Ok(new + { + success = true, + data = result + }); + } + + return BadRequest(new + { + success = false, + errors = _notifier.GetNotifications().Select(n => n.Message) + }); + } + + protected ActionResult CustomResponse(ModelStateDictionary modelState) + { + if (!modelState.IsValid) NotifyErrorModelInvalid(modelState); + return CustomResponse(); + } + + protected void NotifyErrorModelInvalid(ModelStateDictionary modelState) + { + var erros = modelState.Values.SelectMany(e => e.Errors); + foreach (var erro in erros) + { + var errorMsg = erro.Exception == null ? erro.ErrorMessage : erro.Exception.Message; + NotificarErro(errorMsg); + } + } + + protected void NotificarErro(string message) + { + _notifier.Handle(new Notification(message)); + } +} \ No newline at end of file diff --git a/src/Book.Application/Controllers/WeatherForecastController.cs b/src/Book.Application/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..1aa9b725 --- /dev/null +++ b/src/Book.Application/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Book.Application.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/src/Book.Application/Data/ApplicationDbContext.cs b/src/Book.Application/Data/ApplicationDbContext.cs new file mode 100644 index 00000000..f649efb6 --- /dev/null +++ b/src/Book.Application/Data/ApplicationDbContext.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Book.Application.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) : base(options) { } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + => options.UseSqlServer("Server=localhost,1433;Database=BackendTest;User Id=sa;Password=123456Sa;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=true"); + } +} \ No newline at end of file diff --git a/src/Book.Application/Extensions/AppSettings.cs b/src/Book.Application/Extensions/AppSettings.cs new file mode 100644 index 00000000..86fa0d8a --- /dev/null +++ b/src/Book.Application/Extensions/AppSettings.cs @@ -0,0 +1,10 @@ +namespace Book.Application.Extensions +{ + public class AppSettings + { + public string Secret { get; set; } + public int ExpiracaoHoras { get; set; } + public string Emissor { get; set; } + public string ValidoEm { get; set; } + } +} \ No newline at end of file diff --git a/src/Book.Application/Extensions/AspNetUser.cs b/src/Book.Application/Extensions/AspNetUser.cs new file mode 100644 index 00000000..242d8fdc --- /dev/null +++ b/src/Book.Application/Extensions/AspNetUser.cs @@ -0,0 +1,67 @@ +using System.Security.Claims; +using Book.Domain.Interfaces; + +namespace Book.Application.Extensions +{ + public class AspNetUser : IUser + { + private readonly IHttpContextAccessor _accessor; + + public AspNetUser(IHttpContextAccessor accessor) + { + _accessor = accessor; + } + + public string Name => _accessor.HttpContext.User.Identity.Name; + + public Guid GetUserId() + { + return IsAuthenticated() ? Guid.Parse(_accessor.HttpContext.User.GetUserId()) : Guid.Empty; + } + + public string GetUserEmail() + { + return IsAuthenticated() ? _accessor.HttpContext.User.GetUserEmail() : ""; + } + + public bool IsAuthenticated() + { + return _accessor.HttpContext.User.Identity.IsAuthenticated; + } + + public bool IsInRole(string role) + { + return _accessor.HttpContext.User.IsInRole(role); + } + + public IEnumerable GetClaimsIdentity() + { + return _accessor.HttpContext.User.Claims; + } + } + + public static class ClaimsPrincipalExtensions + { + public static string GetUserId(this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + + var claim = principal.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + + public static string GetUserEmail(this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + + var claim = principal.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Extensions/CustomAuthorize.cs b/src/Book.Application/Extensions/CustomAuthorize.cs new file mode 100644 index 00000000..1d717ffe --- /dev/null +++ b/src/Book.Application/Extensions/CustomAuthorize.cs @@ -0,0 +1,48 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Book.Application.Extensions +{ + public class CustomAuthorization + { + public static bool ValidarClaimsUsuario(HttpContext context, string claimName, string claimValue) + { + return context.User.Identity.IsAuthenticated && + context.User.Claims.Any(c => c.Type == claimName && c.Value.Contains(claimValue)); + } + + } + + public class ClaimsAuthorizeAttribute : TypeFilterAttribute + { + public ClaimsAuthorizeAttribute(string claimName, string claimValue) : base(typeof(RequisitoClaimFilter)) + { + Arguments = new object[] { new Claim(claimName, claimValue) }; + } + } + + public class RequisitoClaimFilter : IAuthorizationFilter + { + private readonly Claim _claim; + + public RequisitoClaimFilter(Claim claim) + { + _claim = claim; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext.User.Identity.IsAuthenticated) + { + context.Result = new StatusCodeResult(401); + return; + } + + if (!CustomAuthorization.ValidarClaimsUsuario(context.HttpContext, _claim.Type, _claim.Value)) + { + context.Result = new StatusCodeResult(403); + } + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Extensions/ExceptionMiddleware.cs b/src/Book.Application/Extensions/ExceptionMiddleware.cs new file mode 100644 index 00000000..3d12eccb --- /dev/null +++ b/src/Book.Application/Extensions/ExceptionMiddleware.cs @@ -0,0 +1,32 @@ +using System.Net; + +namespace Book.Application.Extensions +{ + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + + public ExceptionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext httpContext) + { + try + { + await _next(httpContext); + } + catch (Exception ex) + { + HandleExceptionAsync(httpContext, ex); + } + } + + private static void HandleExceptionAsync(HttpContext context, Exception exception) + { + //exception.Ship(context); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Extensions/IdentityMessagesPortuguese.cs b/src/Book.Application/Extensions/IdentityMessagesPortuguese.cs new file mode 100644 index 00000000..fbfae16b --- /dev/null +++ b/src/Book.Application/Extensions/IdentityMessagesPortuguese.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Identity; + +namespace Book.Application.Extensions +{ + public class IdentityMessagesPortuguese : IdentityErrorDescriber + { + public override IdentityError DefaultError() { return new IdentityError { Code = nameof(DefaultError), Description = $"Ocorreu um erro desconhecido." }; } + public override IdentityError ConcurrencyFailure() { return new IdentityError { Code = nameof(ConcurrencyFailure), Description = "Falha de concorrência otimista, o objeto foi modificado." }; } + public override IdentityError PasswordMismatch() { return new IdentityError { Code = nameof(PasswordMismatch), Description = "Senha incorreta." }; } + public override IdentityError InvalidToken() { return new IdentityError { Code = nameof(InvalidToken), Description = "Token inválido." }; } + public override IdentityError LoginAlreadyAssociated() { return new IdentityError { Code = nameof(LoginAlreadyAssociated), Description = "Já existe um usuário com este login." }; } + public override IdentityError InvalidUserName(string userName) { return new IdentityError { Code = nameof(InvalidUserName), Description = $"Login '{userName}' é inválido, pode conter apenas letras ou dígitos." }; } + public override IdentityError InvalidEmail(string email) { return new IdentityError { Code = nameof(InvalidEmail), Description = $"Email '{email}' é inválido." }; } + public override IdentityError DuplicateUserName(string userName) { return new IdentityError { Code = nameof(DuplicateUserName), Description = $"Login '{userName}' já está sendo utilizado." }; } + public override IdentityError DuplicateEmail(string email) { return new IdentityError { Code = nameof(DuplicateEmail), Description = $"Email '{email}' já está sendo utilizado." }; } + public override IdentityError InvalidRoleName(string role) { return new IdentityError { Code = nameof(InvalidRoleName), Description = $"A permissão '{role}' é inválida." }; } + public override IdentityError DuplicateRoleName(string role) { return new IdentityError { Code = nameof(DuplicateRoleName), Description = $"A permissão '{role}' já está sendo utilizada." }; } + public override IdentityError UserAlreadyHasPassword() { return new IdentityError { Code = nameof(UserAlreadyHasPassword), Description = "Usuário já possui uma senha definida." }; } + public override IdentityError UserLockoutNotEnabled() { return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = "Lockout não está habilitado para este usuário." }; } + public override IdentityError UserAlreadyInRole(string role) { return new IdentityError { Code = nameof(UserAlreadyInRole), Description = $"Usuário já possui a permissão '{role}'." }; } + public override IdentityError UserNotInRole(string role) { return new IdentityError { Code = nameof(UserNotInRole), Description = $"Usuário não tem a permissão '{role}'." }; } + public override IdentityError PasswordTooShort(int length) { return new IdentityError { Code = nameof(PasswordTooShort), Description = $"Senhas devem conter ao menos {length} caracteres." }; } + public override IdentityError PasswordRequiresNonAlphanumeric() { return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = "Senhas devem conter ao menos um caracter não alfanumérico." }; } + public override IdentityError PasswordRequiresDigit() { return new IdentityError { Code = nameof(PasswordRequiresDigit), Description = "Senhas devem conter ao menos um digito ('0'-'9')." }; } + public override IdentityError PasswordRequiresLower() { return new IdentityError { Code = nameof(PasswordRequiresLower), Description = "Senhas devem conter ao menos um caracter em caixa baixa ('a'-'z')." }; } + public override IdentityError PasswordRequiresUpper() { return new IdentityError { Code = nameof(PasswordRequiresUpper), Description = "Senhas devem conter ao menos um caracter em caixa alta ('A'-'Z')." }; } + } +} \ No newline at end of file diff --git a/src/Book.Application/Extensions/SqlServerHealthCheck.cs b/src/Book.Application/Extensions/SqlServerHealthCheck.cs new file mode 100644 index 00000000..dd6af62b --- /dev/null +++ b/src/Book.Application/Extensions/SqlServerHealthCheck.cs @@ -0,0 +1,35 @@ +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Book.Application.Extensions +{ + public class SqlServerHealthCheck : IHealthCheck + { + readonly string _connection; + + public SqlServerHealthCheck(string connection) + { + _connection = connection; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) + { + try + { + using (var connection = new SqlConnection(_connection)) + { + await connection.OpenAsync(cancellationToken); + + var command = connection.CreateCommand(); + command.CommandText = "select count(id) from books"; + + return Convert.ToInt32(await command.ExecuteScalarAsync(cancellationToken)) > 0 ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy(); + } + } + catch (Exception) + { + return HealthCheckResult.Unhealthy(); + } + } + } +} \ No newline at end of file diff --git a/src/Book.Application/Migrations/20221204190540_Identity.Designer.cs b/src/Book.Application/Migrations/20221204190540_Identity.Designer.cs new file mode 100644 index 00000000..b8f7891a --- /dev/null +++ b/src/Book.Application/Migrations/20221204190540_Identity.Designer.cs @@ -0,0 +1,282 @@ +// +using System; +using Book.Application.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Book.Application.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20221204190540_Identity")] + partial class Identity + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Book.Application/Migrations/20221204190540_Identity.cs b/src/Book.Application/Migrations/20221204190540_Identity.cs new file mode 100644 index 00000000..a833aee2 --- /dev/null +++ b/src/Book.Application/Migrations/20221204190540_Identity.cs @@ -0,0 +1,221 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Book.Application.Migrations +{ + public partial class Identity : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + ProviderKey = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Book.Application/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Book.Application/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 00000000..b730c4a8 --- /dev/null +++ b/src/Book.Application/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,280 @@ +// +using System; +using Book.Application.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Book.Application.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Book.Application/Program.cs b/src/Book.Application/Program.cs new file mode 100644 index 00000000..37971645 --- /dev/null +++ b/src/Book.Application/Program.cs @@ -0,0 +1,40 @@ +using Book.Application.Configuration; +using Book.Infra.Data.Context; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) + .AddEnvironmentVariables(); + +// ConfigureServices + +builder.Services.AddDbContext(); + +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + +builder.Services.AddIdentityConfig(builder.Configuration); + +builder.Services.AddApiConfig(); + +builder.Services.AddSwaggerConfig(); + +builder.Services.AddLoggingConfig(builder.Configuration); + +builder.Services.ResolveDependencies(); + +var app = builder.Build(); +var apiVersionDescriptionProvider = app.Services.GetRequiredService(); + +// Configure + +app.UseApiConfig(app.Environment); + +app.UseSwaggerConfig(apiVersionDescriptionProvider); + +app.UseLoggingConfiguration(); + +app.Run(); \ No newline at end of file diff --git a/src/Book.Application/Properties/launchSettings.json b/src/Book.Application/Properties/launchSettings.json new file mode 100644 index 00000000..1cdf4cea --- /dev/null +++ b/src/Book.Application/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9090", + "sslPort": 44315 + } + }, + "profiles": { + "Book.Application": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7276;http://localhost:5009", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Book.Application/WeatherForecast.cs b/src/Book.Application/WeatherForecast.cs new file mode 100644 index 00000000..91325d7d --- /dev/null +++ b/src/Book.Application/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Book.Application; + +public class WeatherForecast +{ + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/src/Book.Application/appsettings.Development.json b/src/Book.Application/appsettings.Development.json new file mode 100644 index 00000000..b0676d26 --- /dev/null +++ b/src/Book.Application/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost,1433;Database=BackendTest;User Id=sa;Password=123456Sa;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "AppSettings": { + "Secret": "SUPERSECRETAUTHENTICATIONKEY", + "ExpiracaoHoras": 2, + "Emissor": "Sistema", + "ValidoEm": "https://localhost" + } +} \ No newline at end of file diff --git a/src/Book.Application/appsettings.json b/src/Book.Application/appsettings.json new file mode 100644 index 00000000..b0676d26 --- /dev/null +++ b/src/Book.Application/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost,1433;Database=BackendTest;User Id=sa;Password=123456Sa;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "AppSettings": { + "Secret": "SUPERSECRETAUTHENTICATIONKEY", + "ExpiracaoHoras": 2, + "Emissor": "Sistema", + "ValidoEm": "https://localhost" + } +} \ No newline at end of file diff --git a/src/Book.Domain/Book.Domain.csproj b/src/Book.Domain/Book.Domain.csproj new file mode 100644 index 00000000..bd9813f3 --- /dev/null +++ b/src/Book.Domain/Book.Domain.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/Book.Domain/Interfaces/IBookRepository.cs b/src/Book.Domain/Interfaces/IBookRepository.cs new file mode 100644 index 00000000..21fea9db --- /dev/null +++ b/src/Book.Domain/Interfaces/IBookRepository.cs @@ -0,0 +1,9 @@ +using Book.Domain.Models; + +namespace Book.Domain.Interfaces +{ + public interface IBookRepository : IRepository + { + Task GetBookByName(string name); + } +} \ No newline at end of file diff --git a/src/Book.Domain/Interfaces/IBookService.cs b/src/Book.Domain/Interfaces/IBookService.cs new file mode 100644 index 00000000..a523578a --- /dev/null +++ b/src/Book.Domain/Interfaces/IBookService.cs @@ -0,0 +1,11 @@ +using Book.Domain.Models; + +namespace Book.Domain.Interfaces +{ + public interface IBookService + { + Task Add(BookModel book); + Task Update(BookModel book); + Task Remove(Guid id); + } +} \ No newline at end of file diff --git a/src/Book.Domain/Interfaces/INotifier.cs b/src/Book.Domain/Interfaces/INotifier.cs new file mode 100644 index 00000000..cc56b7f9 --- /dev/null +++ b/src/Book.Domain/Interfaces/INotifier.cs @@ -0,0 +1,11 @@ +using Book.Domain.Notifications; + +namespace Book.Domain.Interfaces +{ + public interface INotifier + { + bool HasNotification(); + List GetNotifications(); + void Handle(Notification notification); + } +} \ No newline at end of file diff --git a/src/Book.Domain/Interfaces/IRepository.cs b/src/Book.Domain/Interfaces/IRepository.cs new file mode 100644 index 00000000..356477ca --- /dev/null +++ b/src/Book.Domain/Interfaces/IRepository.cs @@ -0,0 +1,16 @@ +using System.Linq.Expressions; +using Book.Domain.Models; + +namespace Book.Domain.Interfaces +{ + public interface IRepository : IDisposable where TEntity : Entity + { + Task Add(TEntity entity); + Task GetById(Guid id); + Task> GetAll(); + Task Update(TEntity entity); + Task Remove(Guid id); + Task> Search(Expression> predicate); + Task SaveChanges(); + } +} \ No newline at end of file diff --git a/src/Book.Domain/Interfaces/IUser.cs b/src/Book.Domain/Interfaces/IUser.cs new file mode 100644 index 00000000..46f69d88 --- /dev/null +++ b/src/Book.Domain/Interfaces/IUser.cs @@ -0,0 +1,14 @@ +using System.Security.Claims; + +namespace Book.Domain.Interfaces +{ + public interface IUser + { + string Name { get; } + Guid GetUserId(); + string GetUserEmail(); + bool IsAuthenticated(); + bool IsInRole(string role); + IEnumerable GetClaimsIdentity(); + } +} \ No newline at end of file diff --git a/src/Book.Domain/Models/Book.cs b/src/Book.Domain/Models/Book.cs new file mode 100644 index 00000000..789c66ec --- /dev/null +++ b/src/Book.Domain/Models/Book.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; + +namespace Book.Domain.Models +{ + public class BookModel : Entity + { + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [StringLength(200, ErrorMessage = "O campo {0} precisa ter entre {2} e {1} caracteres", MinimumLength = 2)] + public string Name { get; set; } + + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [StringLength(1000, ErrorMessage = "O campo {0} precisa ter entre {2} e {1} caracteres", MinimumLength = 2)] + public string Description { get; set; } + + public string Image { get; set; } + + [Required(ErrorMessage = "O campo {0} é obrigatório")] + public decimal Value { get; set; } + + public string Genre { get; set; } + + public string Author { get; set; } + + public string Publisher { get; set; } + + public string Edition { get; set; } + + public string Isbn { get; set; } + + public string Language { get; set; } + + public int Pages { get; set; } + + public DateTime DataCadastro { get; set; } + + public bool Active { get; set; } + } +} \ No newline at end of file diff --git a/src/Book.Domain/Models/Entity.cs b/src/Book.Domain/Models/Entity.cs new file mode 100644 index 00000000..6653efdb --- /dev/null +++ b/src/Book.Domain/Models/Entity.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Book.Domain.Models +{ + public class Entity + { + protected Entity() + { + Id = Guid.NewGuid().ToString(); + } + + [Key] + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Book.Domain/Models/User.cs b/src/Book.Domain/Models/User.cs new file mode 100644 index 00000000..55d68213 --- /dev/null +++ b/src/Book.Domain/Models/User.cs @@ -0,0 +1,66 @@ +using System.ComponentModel.DataAnnotations; + +namespace Book.Domain.Models +{ + /*public class User : Entity + { + [Required(ErrorMessage = "O campo {0} é obrigatório")] + public string Login { get; set; } + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [StringLength(200, ErrorMessage = "O campo {0} precisa ter entre {2} e {1} caracteres", MinimumLength = 2)] + public string Name { get; set; } + [Required(ErrorMessage = "O campo {0} é obrigatório")] + public string Email { get; set; } + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [StringLength(100, ErrorMessage = "O campo {0} precisa ter entre {2} e {1} caracteres", MinimumLength = 6)] + public string Password { get; set; } + [Compare("Password", ErrorMessage = "As senhas não conferem.")] + public string ConfirmPassword { get; set; } + public string Role { get; set; } + }*/ + + public class RegisterUser : Entity + { + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [EmailAddress(ErrorMessage = "O campo {0} está em formato inválido")] + public string Email { get; set; } + + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [StringLength(100, ErrorMessage = "O campo {0} precisa ter entre {2} e {1} caracteres", MinimumLength = 6)] + public string Password { get; set; } + + [Compare("Password", ErrorMessage = "As senhas não conferem.")] + public string ConfirmPassword { get; set; } + } + + public class LoginUser : Entity + { + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [EmailAddress(ErrorMessage = "O campo {0} está em formato inválido")] + public string Email { get; set; } + + [Required(ErrorMessage = "O campo {0} é obrigatório")] + [StringLength(100, ErrorMessage = "O campo {0} precisa ter entre {2} e {1} caracteres", MinimumLength = 6)] + public string Password { get; set; } + } + + public class UserToken + { + public string Id { get; set; } + public string Email { get; set; } + public IEnumerable Claims { get; set; } + } + + public class LoginResponseModel : Entity + { + public string AccessToken { get; set; } + public double ExpiresIn { get; set; } + public UserToken UserToken { get; set; } + } + + public class ClaimModel : Entity + { + public string Value { get; set; } + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Book.Domain/Models/Validations/BookValidation.cs b/src/Book.Domain/Models/Validations/BookValidation.cs new file mode 100644 index 00000000..9bb208a4 --- /dev/null +++ b/src/Book.Domain/Models/Validations/BookValidation.cs @@ -0,0 +1,25 @@ +using FluentValidation; + +namespace Book.Domain.Models.Validations +{ + public class BookValidation : AbstractValidator + { + public BookValidation() + { + RuleFor(c => c.Name) + .NotEmpty().WithMessage("O campo {PropertyName} precisa ser fornecido") + .Length(2, 200).WithMessage("O campo {PropertyName} precisa ter entre {MinLength} e {MaxLength} caracteres"); + + RuleFor(c => c.Description) + .NotEmpty().WithMessage("O campo {PropertyName} precisa ser fornecido") + .Length(2, 1000).WithMessage("O campo {PropertyName} precisa ter entre {MinLength} e {MaxLength} caracteres"); + + RuleFor(c => c.Author) + .NotEmpty().WithMessage("O campo {PropertyName} precisa ser fornecido") + .Length(2, 200).WithMessage("O campo {PropertyName} precisa ter entre {MinLength} e {MaxLength} caracteres"); + + RuleFor(c => c.Value) + .GreaterThan(0).WithMessage("O campo {PropertyName} precisa ser maior que {ComparisonValue}"); + } + } +} \ No newline at end of file diff --git a/src/Book.Domain/Models/Validations/Documents/DocsValidation.cs b/src/Book.Domain/Models/Validations/Documents/DocsValidation.cs new file mode 100644 index 00000000..cd0889a7 --- /dev/null +++ b/src/Book.Domain/Models/Validations/Documents/DocsValidation.cs @@ -0,0 +1,176 @@ +namespace Book.Domain.Models.Validations.Documents +{ + public class CpfValidacao + { + public const int TamanhoCpf = 11; + + public static bool Validar(string cpf) + { + var cpfNumeros = Utils.ApenasNumeros(cpf); + + if (!TamanhoValido(cpfNumeros)) return false; + return !TemDigitosRepetidos(cpfNumeros) && TemDigitosValidos(cpfNumeros); + } + + private static bool TamanhoValido(string valor) + { + return valor.Length == TamanhoCpf; + } + + private static bool TemDigitosRepetidos(string valor) + { + string[] invalidNumbers = + { + "00000000000", + "11111111111", + "22222222222", + "33333333333", + "44444444444", + "55555555555", + "66666666666", + "77777777777", + "88888888888", + "99999999999" + }; + return invalidNumbers.Contains(valor); + } + + private static bool TemDigitosValidos(string valor) + { + var number = valor.Substring(0, TamanhoCpf - 2); + var digitoVerificador = new DigitoVerificador(number) + .ComMultiplicadoresDeAte(2, 11) + .Substituindo("0", 10, 11); + var firstDigit = digitoVerificador.CalculaDigito(); + digitoVerificador.AddDigito(firstDigit); + var secondDigit = digitoVerificador.CalculaDigito(); + + return string.Concat(firstDigit, secondDigit) == valor.Substring(TamanhoCpf - 2, 2); + } + } + + public class CnpjValidacao + { + public const int TamanhoCnpj = 14; + + public static bool Validar(string cpnj) + { + var cnpjNumeros = Utils.ApenasNumeros(cpnj); + + if (!TemTamanhoValido(cnpjNumeros)) return false; + return !TemDigitosRepetidos(cnpjNumeros) && TemDigitosValidos(cnpjNumeros); + } + + private static bool TemTamanhoValido(string valor) + { + return valor.Length == TamanhoCnpj; + } + + private static bool TemDigitosRepetidos(string valor) + { + string[] invalidNumbers = + { + "00000000000000", + "11111111111111", + "22222222222222", + "33333333333333", + "44444444444444", + "55555555555555", + "66666666666666", + "77777777777777", + "88888888888888", + "99999999999999" + }; + return invalidNumbers.Contains(valor); + } + + private static bool TemDigitosValidos(string valor) + { + var number = valor.Substring(0, TamanhoCnpj - 2); + + var digitoVerificador = new DigitoVerificador(number) + .ComMultiplicadoresDeAte(2, 9) + .Substituindo("0", 10, 11); + var firstDigit = digitoVerificador.CalculaDigito(); + digitoVerificador.AddDigito(firstDigit); + var secondDigit = digitoVerificador.CalculaDigito(); + + return string.Concat(firstDigit, secondDigit) == valor.Substring(TamanhoCnpj - 2, 2); + } + } + + public class DigitoVerificador + { + private string _numero; + private const int Modulo = 11; + private readonly List _multiplicadores = new List { 2, 3, 4, 5, 6, 7, 8, 9 }; + private readonly IDictionary _substituicoes = new Dictionary(); + private bool _complementarDoModulo = true; + + public DigitoVerificador(string numero) + { + _numero = numero; + } + + public DigitoVerificador ComMultiplicadoresDeAte(int primeiroMultiplicador, int ultimoMultiplicador) + { + _multiplicadores.Clear(); + for (var i = primeiroMultiplicador; i <= ultimoMultiplicador; i++) + _multiplicadores.Add(i); + + return this; + } + + public DigitoVerificador Substituindo(string substituto, params int[] digitos) + { + foreach (var i in digitos) + { + _substituicoes[i] = substituto; + } + return this; + } + + public void AddDigito(string digito) + { + _numero = string.Concat(_numero, digito); + } + + public string CalculaDigito() + { + return !(_numero.Length > 0) ? "" : GetDigitSum(); + } + + private string GetDigitSum() + { + var soma = 0; + for (int i = _numero.Length - 1, m = 0; i >= 0; i--) + { + var produto = (int)char.GetNumericValue(_numero[i]) * _multiplicadores[m]; + soma += produto; + + if (++m >= _multiplicadores.Count) m = 0; + } + + var mod = (soma % Modulo); + var resultado = _complementarDoModulo ? Modulo - mod : mod; + + return _substituicoes.ContainsKey(resultado) ? _substituicoes[resultado] : resultado.ToString(); + } + } + + public class Utils + { + public static string ApenasNumeros(string valor) + { + var onlyNumber = ""; + foreach (var s in valor) + { + if (char.IsDigit(s)) + { + onlyNumber += s; + } + } + return onlyNumber.Trim(); + } + } +} \ No newline at end of file diff --git a/src/Book.Domain/Notifications/Notification.cs b/src/Book.Domain/Notifications/Notification.cs new file mode 100644 index 00000000..91e71924 --- /dev/null +++ b/src/Book.Domain/Notifications/Notification.cs @@ -0,0 +1,12 @@ +namespace Book.Domain.Notifications +{ + public class Notification + { + public Notification(string message) + { + Message = message; + } + + public string Message { get; } + } +} \ No newline at end of file diff --git a/src/Book.Domain/Notifications/Notifier.cs b/src/Book.Domain/Notifications/Notifier.cs new file mode 100644 index 00000000..12604001 --- /dev/null +++ b/src/Book.Domain/Notifications/Notifier.cs @@ -0,0 +1,29 @@ +using Book.Domain.Interfaces; + +namespace Book.Domain.Notifications +{ + public class Notifier : INotifier + { + private List _notifications; + + public Notifier() + { + _notifications = new List(); + } + + public void Handle(Notification notification) + { + _notifications.Add(notification); + } + + public List GetNotifications() + { + return _notifications; + } + + public bool HasNotification() + { + return _notifications.Any(); + } + } +} \ No newline at end of file diff --git a/src/Book.Domain/obj/Debug/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs b/src/Book.Domain/obj/Debug/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs new file mode 100644 index 00000000..36203c72 --- /dev/null +++ b/src/Book.Domain/obj/Debug/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] diff --git a/src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfo.cs b/src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfo.cs new file mode 100644 index 00000000..5266be7a --- /dev/null +++ b/src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfo.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// O código foi gerado por uma ferramenta. +// Versão de Tempo de Execução:4.0.30319.42000 +// +// As alterações ao arquivo poderão causar comportamento incorreto e serão perdidas se +// o código for gerado novamente. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("BackendTest")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("BackendTest")] +[assembly: System.Reflection.AssemblyTitleAttribute("BackendTest")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Gerado pela classe WriteCodeFragment do MSBuild. + diff --git a/src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfoInputs.cache b/src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfoInputs.cache new file mode 100644 index 00000000..333d8982 --- /dev/null +++ b/src/Book.Domain/obj/Debug/net6.0/BackendTest.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +cc685fb592b82f984b83cf0b35f38546024dfdfd diff --git a/src/Book.Domain/obj/Debug/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig b/src/Book.Domain/obj/Debug/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 00000000..5e32ec68 --- /dev/null +++ b/src/Book.Domain/obj/Debug/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,16 @@ +is_global = true +build_property.TargetFramework = net6.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = true +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = BackendTest +build_property.RootNamespace = BackendTest +build_property.ProjectDir = E:\Projetos\helyon\src\ +build_property.RazorLangVersion = 6.0 +build_property.SupportLocalizedComponentNames = +build_property.GenerateRazorMetadataSourceChecksumAttributes = +build_property.MSBuildProjectDirectory = E:\Projetos\helyon\src +build_property._RazorSourceGeneratorDebug = diff --git a/src/Book.Domain/obj/Debug/net6.0/BackendTest.GlobalUsings.g.cs b/src/Book.Domain/obj/Debug/net6.0/BackendTest.GlobalUsings.g.cs new file mode 100644 index 00000000..025530a2 --- /dev/null +++ b/src/Book.Domain/obj/Debug/net6.0/BackendTest.GlobalUsings.g.cs @@ -0,0 +1,17 @@ +// +global using global::Microsoft.AspNetCore.Builder; +global using global::Microsoft.AspNetCore.Hosting; +global using global::Microsoft.AspNetCore.Http; +global using global::Microsoft.AspNetCore.Routing; +global using global::Microsoft.Extensions.Configuration; +global using global::Microsoft.Extensions.DependencyInjection; +global using global::Microsoft.Extensions.Hosting; +global using global::Microsoft.Extensions.Logging; +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Net.Http.Json; +global using global::System.Threading; +global using global::System.Threading.Tasks; diff --git a/src/Book.Domain/obj/Debug/net6.0/BackendTest.csproj.AssemblyReference.cache b/src/Book.Domain/obj/Debug/net6.0/BackendTest.csproj.AssemblyReference.cache new file mode 100644 index 0000000000000000000000000000000000000000..df86f3a463ec265dc3cf330d7c93907a926fbf98 GIT binary patch literal 173738 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 literal 0 HcmV?d00001 diff --git a/src/Book.Domain/obj/Release/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs b/src/Book.Domain/obj/Release/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs new file mode 100644 index 00000000..36203c72 --- /dev/null +++ b/src/Book.Domain/obj/Release/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] diff --git a/src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfo.cs b/src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfo.cs new file mode 100644 index 00000000..00475224 --- /dev/null +++ b/src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfo.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// O código foi gerado por uma ferramenta. +// Versão de Tempo de Execução:4.0.30319.42000 +// +// As alterações ao arquivo poderão causar comportamento incorreto e serão perdidas se +// o código for gerado novamente. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("BackendTest")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("BackendTest")] +[assembly: System.Reflection.AssemblyTitleAttribute("BackendTest")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Gerado pela classe WriteCodeFragment do MSBuild. + diff --git a/src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfoInputs.cache b/src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfoInputs.cache new file mode 100644 index 00000000..90561dfa --- /dev/null +++ b/src/Book.Domain/obj/Release/net6.0/BackendTest.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +2d6e6e7ca999bc4c992e1932c4ac7b1d639ee2cb diff --git a/src/Book.Domain/obj/Release/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig b/src/Book.Domain/obj/Release/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 00000000..5e32ec68 --- /dev/null +++ b/src/Book.Domain/obj/Release/net6.0/BackendTest.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,16 @@ +is_global = true +build_property.TargetFramework = net6.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = true +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = BackendTest +build_property.RootNamespace = BackendTest +build_property.ProjectDir = E:\Projetos\helyon\src\ +build_property.RazorLangVersion = 6.0 +build_property.SupportLocalizedComponentNames = +build_property.GenerateRazorMetadataSourceChecksumAttributes = +build_property.MSBuildProjectDirectory = E:\Projetos\helyon\src +build_property._RazorSourceGeneratorDebug = diff --git a/src/Book.Domain/obj/Release/net6.0/BackendTest.GlobalUsings.g.cs b/src/Book.Domain/obj/Release/net6.0/BackendTest.GlobalUsings.g.cs new file mode 100644 index 00000000..025530a2 --- /dev/null +++ b/src/Book.Domain/obj/Release/net6.0/BackendTest.GlobalUsings.g.cs @@ -0,0 +1,17 @@ +// +global using global::Microsoft.AspNetCore.Builder; +global using global::Microsoft.AspNetCore.Hosting; +global using global::Microsoft.AspNetCore.Http; +global using global::Microsoft.AspNetCore.Routing; +global using global::Microsoft.Extensions.Configuration; +global using global::Microsoft.Extensions.DependencyInjection; +global using global::Microsoft.Extensions.Hosting; +global using global::Microsoft.Extensions.Logging; +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Net.Http.Json; +global using global::System.Threading; +global using global::System.Threading.Tasks; diff --git a/src/Book.Domain/obj/Release/net6.0/BackendTest.csproj.AssemblyReference.cache b/src/Book.Domain/obj/Release/net6.0/BackendTest.csproj.AssemblyReference.cache new file mode 100644 index 0000000000000000000000000000000000000000..df86f3a463ec265dc3cf330d7c93907a926fbf98 GIT binary patch literal 173738 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 literal 0 HcmV?d00001 diff --git a/src/Book.Infra.Data/Book.Infra.Data.csproj b/src/Book.Infra.Data/Book.Infra.Data.csproj new file mode 100644 index 00000000..0e8463b8 --- /dev/null +++ b/src/Book.Infra.Data/Book.Infra.Data.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Book.Infra.Data/Context/BookApiDbContext.cs b/src/Book.Infra.Data/Context/BookApiDbContext.cs new file mode 100644 index 00000000..b1a5bd13 --- /dev/null +++ b/src/Book.Infra.Data/Context/BookApiDbContext.cs @@ -0,0 +1,64 @@ +using Book.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Book.Infra.Data.Context +{ + public class BookApiDbContext : DbContext + { + public BookApiDbContext(DbContextOptions options) : base(options) + { + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + ChangeTracker.AutoDetectChangesEnabled = false; + } + + public DbSet Books { get; set; } + //public DbSet Users { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + => options.UseSqlServer("Server=localhost,1433;Database=BackendTest;User Id=sa;Password=123456Sa;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=true"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + foreach (var property in modelBuilder.Model.GetEntityTypes() + .SelectMany(e => e.GetProperties() + .Where(p => p.ClrType == typeof(string)))) + property.SetColumnType("varchar(100)"); + + modelBuilder.ApplyConfigurationsFromAssembly(typeof(BookApiDbContext).Assembly); + + foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys())) relationship.DeleteBehavior = DeleteBehavior.ClientSetNull; + + base.OnModelCreating(modelBuilder); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + { + foreach (var entry in ChangeTracker.Entries().Where(entry => entry.Entity.GetType().GetProperty("DataCadastro") != null)) + { + if (entry.State == EntityState.Added) + { + entry.Property("DataCadastro").CurrentValue = DateTime.Now; + } + + if (entry.State == EntityState.Modified) + { + entry.Property("DataCadastro").IsModified = false; + } + } + + return base.SaveChangesAsync(cancellationToken); + } + } + + public class BookApiDbContextFactory : IDesignTimeDbContextFactory + { + public BookApiDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer("DefaultConnection"); + + return new BookApiDbContext(optionsBuilder.Options); + } + } +} \ No newline at end of file diff --git a/src/Book.Infra.Data/Mappings/BookMapping.cs b/src/Book.Infra.Data/Mappings/BookMapping.cs new file mode 100644 index 00000000..c3d4945c --- /dev/null +++ b/src/Book.Infra.Data/Mappings/BookMapping.cs @@ -0,0 +1,28 @@ +using Book.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Book.Infra.Data.Mappings +{ + public class BookMapping : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(p => p.Id); + + builder.Property(p => p.Name) + .IsRequired() + .HasColumnType("varchar(200)"); + + builder.Property(p => p.Description) + .IsRequired() + .HasColumnType("varchar(1000)"); + + builder.Property(p => p.Image) + .IsRequired() + .HasColumnType("varchar(100)"); + + builder.ToTable("Books"); + } + } +} \ No newline at end of file diff --git a/src/Book.Infra.Data/Migrations/20221204190009_Initial.Designer.cs b/src/Book.Infra.Data/Migrations/20221204190009_Initial.Designer.cs new file mode 100644 index 00000000..e16477b3 --- /dev/null +++ b/src/Book.Infra.Data/Migrations/20221204190009_Initial.Designer.cs @@ -0,0 +1,89 @@ +// +using System; +using Book.Infra.Data.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Book.Infra.Data.Migrations +{ + [DbContext(typeof(BookApiDbContext))] + [Migration("20221204190009_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Book.Domain.Models.BookModel", b => + { + b.Property("Id") + .HasColumnType("varchar(100)"); + + b.Property("Active") + .HasColumnType("bit"); + + b.Property("Author") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("DataCadastro") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Edition") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Genre") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Image") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Isbn") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Language") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Pages") + .HasColumnType("int"); + + b.Property("Publisher") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Value") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.ToTable("Books", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Book.Infra.Data/Migrations/20221204190009_Initial.cs b/src/Book.Infra.Data/Migrations/20221204190009_Initial.cs new file mode 100644 index 00000000..f00d8d8c --- /dev/null +++ b/src/Book.Infra.Data/Migrations/20221204190009_Initial.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Book.Infra.Data.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Books", + columns: table => new + { + Id = table.Column(type: "varchar(100)", nullable: false), + Name = table.Column(type: "varchar(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: false), + Image = table.Column(type: "varchar(100)", nullable: false), + Value = table.Column(type: "decimal(18,2)", nullable: false), + Genre = table.Column(type: "varchar(100)", nullable: false), + Author = table.Column(type: "varchar(100)", nullable: false), + Publisher = table.Column(type: "varchar(100)", nullable: false), + Edition = table.Column(type: "varchar(100)", nullable: false), + Isbn = table.Column(type: "varchar(100)", nullable: false), + Language = table.Column(type: "varchar(100)", nullable: false), + Pages = table.Column(type: "int", nullable: false), + DataCadastro = table.Column(type: "datetime2", nullable: false), + Active = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Books"); + } + } +} diff --git a/src/Book.Infra.Data/Migrations/BookApiDbContextModelSnapshot.cs b/src/Book.Infra.Data/Migrations/BookApiDbContextModelSnapshot.cs new file mode 100644 index 00000000..1c28bc84 --- /dev/null +++ b/src/Book.Infra.Data/Migrations/BookApiDbContextModelSnapshot.cs @@ -0,0 +1,87 @@ +// +using System; +using Book.Infra.Data.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Book.Infra.Data.Migrations +{ + [DbContext(typeof(BookApiDbContext))] + partial class BookApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Book.Domain.Models.BookModel", b => + { + b.Property("Id") + .HasColumnType("varchar(100)"); + + b.Property("Active") + .HasColumnType("bit"); + + b.Property("Author") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("DataCadastro") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Edition") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Genre") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Image") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Isbn") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Language") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Pages") + .HasColumnType("int"); + + b.Property("Publisher") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("Value") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.ToTable("Books", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Book.Infra.Data/Repository/BookRepository.cs b/src/Book.Infra.Data/Repository/BookRepository.cs new file mode 100644 index 00000000..1d145f3b --- /dev/null +++ b/src/Book.Infra.Data/Repository/BookRepository.cs @@ -0,0 +1,19 @@ +using Book.Domain.Interfaces; +using Book.Domain.Models; +using Book.Infra.Data.Context; +using Microsoft.EntityFrameworkCore; + +namespace Book.Infra.Data.Repository +{ + public class BookRepository : Repository, IBookRepository + { + public BookRepository(BookApiDbContext context) : base(context) { } + + public async Task GetBookByName(string name) + { + return await Db.Books.AsNoTracking() + .Include(c => c.Name) + .FirstOrDefaultAsync(c => c.Name == name); + } + } +} \ No newline at end of file diff --git a/src/Book.Infra.Data/Repository/Repository.cs b/src/Book.Infra.Data/Repository/Repository.cs new file mode 100644 index 00000000..6f172428 --- /dev/null +++ b/src/Book.Infra.Data/Repository/Repository.cs @@ -0,0 +1,63 @@ +using System.Linq.Expressions; +using Book.Domain.Interfaces; +using Book.Domain.Models; +using Book.Infra.Data.Context; +using Microsoft.EntityFrameworkCore; + +namespace Book.Infra.Data.Repository +{ + public abstract class Repository : IRepository where TEntity : Entity, new() + { + protected readonly BookApiDbContext Db; + protected readonly DbSet DbSet; + + protected Repository(BookApiDbContext db) + { + Db = db; + DbSet = db.Set(); + } + + public async Task> Search(Expression> predicate) + { + return await DbSet.AsNoTracking().Where(predicate).ToListAsync(); + } + + public virtual async Task GetById(Guid id) + { + return await DbSet.FindAsync(id); + } + + public virtual async Task> GetAll() + { + return await DbSet.ToListAsync(); + } + + public virtual async Task Add(TEntity entity) + { + DbSet.Add(entity); + await SaveChanges(); + } + + public virtual async Task Update(TEntity entity) + { + DbSet.Update(entity); + await SaveChanges(); + } + + public virtual async Task Remove(Guid id) + { + DbSet.Remove(new TEntity { Id = id.ToString() }); + await SaveChanges(); + } + + public async Task SaveChanges() + { + return await Db.SaveChangesAsync(); + } + + public void Dispose() + { + Db?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Book.Service/Book.Service.csproj b/src/Book.Service/Book.Service.csproj new file mode 100644 index 00000000..7cb26e6a --- /dev/null +++ b/src/Book.Service/Book.Service.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net6.0 + enable + enable + + + diff --git a/src/Book.Service/Services/BaseService.cs b/src/Book.Service/Services/BaseService.cs new file mode 100644 index 00000000..3aaefc16 --- /dev/null +++ b/src/Book.Service/Services/BaseService.cs @@ -0,0 +1,42 @@ +using Book.Domain.Interfaces; +using Book.Domain.Models; +using Book.Domain.Notifications; +using FluentValidation; +using FluentValidation.Results; + +namespace Book.Service.Services +{ + public class BaseService + { + private readonly INotifier _notifier; + + protected BaseService(INotifier notifier) + { + _notifier = notifier; + } + + protected void Notify(ValidationResult validationResult) + { + foreach (var error in validationResult.Errors) + { + Notify(error.ErrorMessage); + } + } + + protected void Notify(string message) + { + _notifier.Handle(new Notification(message)); + } + + protected bool ExecuteValidation(TV validation, TE entity) where TV : AbstractValidator where TE : Entity + { + var validator = validation.Validate(entity); + + if (validator.IsValid) return true; + + Notify(validator); + + return false; + } + } +} \ No newline at end of file diff --git a/src/Book.Service/Services/BookService.cs b/src/Book.Service/Services/BookService.cs new file mode 100644 index 00000000..d300da11 --- /dev/null +++ b/src/Book.Service/Services/BookService.cs @@ -0,0 +1,40 @@ +using Book.Domain.Interfaces; +using Book.Domain.Models; +using Book.Domain.Models.Validations; + +namespace Book.Service.Services +{ + public class BookService : BaseService, IBookService + { + private readonly IBookRepository _bookRepository; + + public BookService(IBookRepository bookRepository, INotifier notifier) : base(notifier) + { + _bookRepository = bookRepository; + } + + public async Task Add(BookModel book) + { + if (!ExecuteValidation(new BookValidation(), book)) return; + + await _bookRepository.Add(book); + } + + public async Task Remove(Guid id) + { + await _bookRepository.Remove(id); + } + + public async Task Update(BookModel book) + { + if (!ExecuteValidation(new BookValidation(), book)) return; + + await _bookRepository.Update(book); + } + + public void Dispose() + { + _bookRepository?.Dispose(); + } + } +} \ No newline at end of file