diff --git a/Equinox.sln b/Equinox.sln index f31ef25a..3d09df33 100644 --- a/Equinox.sln +++ b/Equinox.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +VisualStudioVersion = 15.0.26430.13 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AE3E98DB-35C1-42D8-AB6D-DE37189DDAA1}" EndProject @@ -12,15 +12,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1 - Presentation", "1 - Presentation", "{9340B0E0-D3D9-4917-B0AA-093A95183265}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 - Application", "2 - Application", "{8941C48B-849B-4625-B5F8-0EF567135BC0}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 - Application", "3 - Application", "{8941C48B-849B-4625-B5F8-0EF567135BC0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 - Domain", "3 - Domain", "{988F2F26-89C2-4C44-B1BB-42C394832A4E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 - Domain", "4 - Domain", "{988F2F26-89C2-4C44-B1BB-42C394832A4E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 - Infra", "4 - Infra", "{04CCC7F8-8AF7-4965-B2EF-2E28331BE421}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 - Infra", "5 - Infra", "{04CCC7F8-8AF7-4965-B2EF-2E28331BE421}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4.1 - Data", "4.1 - Data", "{DCCD4FD6-B437-4586-B932-7AC40EF86A71}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.1 - Data", "5.1 - Data", "{DCCD4FD6-B437-4586-B932-7AC40EF86A71}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4.2 - CrossCutting", "4.2 - CrossCutting", "{68258835-A222-468B-84F5-441AD58BCD51}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.2 - CrossCutting", "5.2 - CrossCutting", "{68258835-A222-468B-84F5-441AD58BCD51}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.UI.Site", "src\Equinox.UI.Site\Equinox.UI.Site.csproj", "{E38E811D-9A10-41EF-A245-907A71C79ACB}" EndProject @@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Infra.CrossCutting. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.WebApi", "src\Equinox.WebApi\Equinox.WebApi.csproj", "{918C9F92-CA9C-4481-9AFA-8DD5A1A810D3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 - Services", "2 - Services", "{A5EBFB52-C5C5-4F98-B441-E48DA9CA5744}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +103,7 @@ Global {9330566C-4093-4FC7-AF61-14850DD15793} = {68258835-A222-468B-84F5-441AD58BCD51} {A9CB4A5C-DCF4-4F87-83C2-2D6A34C6E2BA} = {988F2F26-89C2-4C44-B1BB-42C394832A4E} {7C9495CE-AEC5-44CB-9D9C-1150DF99F817} = {68258835-A222-468B-84F5-441AD58BCD51} - {918C9F92-CA9C-4481-9AFA-8DD5A1A810D3} = {9340B0E0-D3D9-4917-B0AA-093A95183265} + {918C9F92-CA9C-4481-9AFA-8DD5A1A810D3} = {A5EBFB52-C5C5-4F98-B441-E48DA9CA5744} + {A5EBFB52-C5C5-4F98-B441-E48DA9CA5744} = {AE3E98DB-35C1-42D8-AB6D-DE37189DDAA1} EndGlobalSection EndGlobal diff --git a/src/Equinox.Infra.Data/Repository/CustomerRepository.cs b/src/Equinox.Infra.Data/Repository/CustomerRepository.cs index 700d8ebf..c358268e 100644 --- a/src/Equinox.Infra.Data/Repository/CustomerRepository.cs +++ b/src/Equinox.Infra.Data/Repository/CustomerRepository.cs @@ -2,6 +2,7 @@ using Equinox.Domain.Interfaces; using Equinox.Domain.Models; using Equinox.Infra.Data.Context; +using Microsoft.EntityFrameworkCore; namespace Equinox.Infra.Data.Repository { diff --git a/src/Equinox.WebApi/Configurations/RouteConvention.cs b/src/Equinox.WebApi/Configurations/RouteConvention.cs new file mode 100644 index 00000000..e7fb5958 --- /dev/null +++ b/src/Equinox.WebApi/Configurations/RouteConvention.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Equinox.WebApi.Configurations +{ + public class RouteConvention : IApplicationModelConvention + { + private readonly AttributeRouteModel _centralPrefix; + + public RouteConvention(IRouteTemplateProvider routeTemplateProvider) + { + _centralPrefix = new AttributeRouteModel(routeTemplateProvider); + } + + public void Apply(ApplicationModel application) + { + foreach (var controller in application.Controllers) + { + var matchedSelectors = controller.Selectors.Where(x => x.AttributeRouteModel != null).ToList(); + if (matchedSelectors.Any()) + { + foreach (var selectorModel in matchedSelectors) + { + selectorModel.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_centralPrefix, + selectorModel.AttributeRouteModel); + } + } + + var unmatchedSelectors = controller.Selectors.Where(x => x.AttributeRouteModel == null).ToList(); + + if (unmatchedSelectors.Any()) + { + foreach (var selectorModel in unmatchedSelectors) + { + selectorModel.AttributeRouteModel = _centralPrefix; + } + } + } + } + } + + public static class MvcOptionsExtensions + { + public static void UseCentralRoutePrefix(this MvcOptions opts, IRouteTemplateProvider routeAttribute) + { + opts.Conventions.Insert(0, new RouteConvention(routeAttribute)); + } + } +} \ No newline at end of file diff --git a/src/Equinox.WebApi/Controllers/AccountController.cs b/src/Equinox.WebApi/Controllers/AccountController.cs index 93c443bf..faa39c9c 100644 --- a/src/Equinox.WebApi/Controllers/AccountController.cs +++ b/src/Equinox.WebApi/Controllers/AccountController.cs @@ -1,426 +1,80 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; +using Equinox.Domain.Core.Notifications; using Equinox.Infra.CrossCutting.Identity.Models; using Equinox.Infra.CrossCutting.Identity.Models.AccountViewModels; -using Equinox.Infra.CrossCutting.Identity.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Logging; namespace Equinox.WebApi.Controllers { [Authorize] - public class AccountController : ControllerBase + public class AccountController : ApiController { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly IEmailSender _emailSender; - private readonly ISmsSender _smsSender; private readonly ILogger _logger; public AccountController( UserManager userManager, SignInManager signInManager, - IEmailSender emailSender, - ISmsSender smsSender, - ILoggerFactory loggerFactory) + IDomainNotificationHandler notifications, + ILoggerFactory loggerFactory) : base(notifications) { _userManager = userManager; _signInManager = signInManager; - _emailSender = emailSender; - _smsSender = smsSender; _logger = loggerFactory.CreateLogger(); } - // - // POST: /Account/Login [HttpPost] [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task Login(LoginViewModel model) - { - if (ModelState.IsValid) - { - // This doesn't count login failures towards account lockout - // To enable password failures to trigger account lockout, set lockoutOnFailure: true - var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); - if (result.Succeeded) - { - _logger.LogInformation(1, "User logged in."); - return Ok(); - } - if (result.RequiresTwoFactor) - { - return RedirectToAction(nameof(SendCode), new { RememberMe = model.RememberMe }); - } - if (result.IsLockedOut) - { - _logger.LogWarning(2, "User account locked out."); - return Unauthorized(); - } - else - { - ModelState.AddModelError(string.Empty, "Invalid login attempt."); - return BadRequest(model); - } - } - - // If we got this far, something failed, redisplay form - return BadRequest(model); - } - - // - // POST: /Account/Register - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task Register(RegisterViewModel model) - { - if (ModelState.IsValid) - { - var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; - - // User claim for write customers data - user.Claims.Add(new IdentityUserClaim - { - ClaimType = "Customers", - ClaimValue = "Write" - }); - - var result = await _userManager.CreateAsync(user, model.Password); - if (result.Succeeded) - { - // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 - // Send an email with this link - //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); - //var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); - //await _emailSender.SendEmailAsync(model.Email, "Confirm your account", - // $"Please confirm your account by clicking this link: link"); - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(3, "User created a new account with password."); - return Ok(); - } - AddErrors(result); - } - - // If we got this far, something failed, redisplay form - return BadRequest(model); - } - - // - // POST: /Account/LogOff - [HttpPost] - [ValidateAntiForgeryToken] - public async Task LogOff() - { - await _signInManager.SignOutAsync(); - _logger.LogInformation(4, "User logged out."); - return SignOut(); - } - - // - // POST: /Account/ExternalLogin - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public IActionResult ExternalLogin(string provider, string returnUrl = null) - { - // Request a redirect to the external login provider. - var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }); - var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); - var result = Challenge(properties, provider); - return result; - } - - [HttpGet] - [AllowAnonymous] - public async Task ExternalLoginLogOff(string provider, string returnUrl = null) - { - await _signInManager.SignOutAsync(); - _logger.LogInformation(4, "User logged out."); - return RedirectToAction("ExternalLogin", "Account", new { provider, returnUrl }); - } - - // - // GET: /Account/ExternalLoginCallback - [HttpGet] - [AllowAnonymous] - public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) - { - if (remoteError != null) - { - ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}"); - return BadRequest(ModelState); - } - var info = await _signInManager.GetExternalLoginInfoAsync(); - if (info == null) - { - return InternalServerError(); - } - - // Sign in the user with this external login provider if the user already has a login. - var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); - if (result.Succeeded) - { - _logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider); - return Ok(); - } - if (result.IsLockedOut) - { - return BadRequest("Lockout"); - } - else - { - // If the user does not have an account - return BadRequest(); - } - } - - // - // POST: /Account/ExternalLoginConfirmation - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model) - { - if (ModelState.IsValid) - { - // Get the information about the user from the external login provider - var info = await _signInManager.GetExternalLoginInfoAsync(); - if (info == null) - { - return BadRequest("ExternalLoginFailure"); - } - var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; - - // User claim for write customers data - user.Claims.Add(new IdentityUserClaim - { - ClaimType = "Customers", - ClaimValue = "Write" - }); - - var result = await _userManager.CreateAsync(user); - if (result.Succeeded) - { - result = await _userManager.AddLoginAsync(user, info); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider); - return Ok(); - } - } - AddErrors(result); - } - - return Ok(model); - } - - // GET: /Account/ConfirmEmail - [HttpGet] - [AllowAnonymous] - public async Task ConfirmEmail(string userId, string code) - { - if (userId == null || code == null) - { - return BadRequest("Error"); - } - var user = await _userManager.FindByIdAsync(userId); - if (user == null) - { - return NotFound(); - } - var result = await _userManager.ConfirmEmailAsync(user, code); - if (!result.Succeeded) - return BadRequest(); - - return Ok(); - } - - // - // POST: /Account/ForgotPassword - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task ForgotPassword(ForgotPasswordViewModel model) - { - if (ModelState.IsValid) - { - var user = await _userManager.FindByNameAsync(model.Email); - if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) - { - // Don't reveal that the user does not exist or is not confirmed - return Ok(); - } - - // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 - // Send an email with this link - //var code = await _userManager.GeneratePasswordResetTokenAsync(user); - //var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); - //await _emailSender.SendEmailAsync(model.Email, "Reset Password", - // $"Please reset your password by clicking here: link"); - //return View("ForgotPasswordConfirmation"); - } - - // If we got this far, something failed, redisplay form - return Ok(); - } - - // - // POST: /Account/ResetPassword - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task ResetPassword(ResetPasswordViewModel model) + [Route("account")] + public async Task Login([FromBody] LoginViewModel model) { if (!ModelState.IsValid) { - return BadRequest(model); - } - var user = await _userManager.FindByNameAsync(model.Email); - if (user == null) - { - // Don't reveal that the user does not exist - return Ok(); + NotifyModelStateErrors(); + return Response(model); } - var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); - if (result.Succeeded) - { - return Ok(); - } - AddErrors(result); - if (!ModelState.IsValid) - return BadRequest(); - - return Ok(); - } + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, true); + if (!result.Succeeded) + NotifyError(result.ToString(), "Login failure"); - // - // GET: /Account/SendCode - [HttpGet] - [AllowAnonymous] - public async Task SendCode(string returnUrl = null, bool rememberMe = false) - { - var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - return BadRequest("Error"); - } - var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); - var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList(); - return Ok(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe }); + _logger.LogInformation(1, "User logged in."); + return Response(model); } - // - // POST: /Account/SendCode [HttpPost] [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task SendCode(SendCodeViewModel model) + [Route("account/register")] + public async Task Register(RegisterViewModel model) { if (!ModelState.IsValid) { - return BadRequest(); - } - - var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - return InternalServerError(); - } - - // Generate the token and send it - var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider); - if (string.IsNullOrWhiteSpace(code)) - { - return InternalServerError(); - } - - var message = "Your security code is: " + code; - if (model.SelectedProvider == "Email") - { - await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message); - } - else if (model.SelectedProvider == "Phone") - { - await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message); + NotifyModelStateErrors(); + return Response(model); } - return RedirectToAction(nameof(VerifyCode), new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); - } + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; - // - // GET: /Account/VerifyCode - [HttpGet] - [AllowAnonymous] - public async Task VerifyCode(string provider, bool rememberMe) - { - // Require that the user has already logged in via username/password or external login - var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - return BadRequest(); - } - return Ok(new VerifyCodeViewModel { Provider = provider, RememberMe = rememberMe }); - } + // User claim for write customers data + user.Claims.Add(new IdentityUserClaim { ClaimType = "Customers", ClaimValue = "Read" }); + user.Claims.Add(new IdentityUserClaim { ClaimType = "Customers", ClaimValue = "Write" }); - // - // POST: /Account/VerifyCode - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task VerifyCode(VerifyCodeViewModel model) - { - if (!ModelState.IsValid) - { - return BadRequest(model); - } - // The following code protects for brute force attacks against the two factor codes. - // If a user enters incorrect codes for a specified amount of time then the user account - // will be locked out for a specified amount of time. - var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); + var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { - return Ok(); + await _signInManager.SignInAsync(user, false); + _logger.LogInformation(3, "User created a new account with password."); + return Response(model); } - if (result.IsLockedOut) - { - _logger.LogWarning(7, "User account locked out."); - return Unauthorized(); - } - else - { - ModelState.AddModelError(string.Empty, "Invalid code."); - return BadRequest(model); - } - } - - #region Helpers - private void AddErrors(IdentityResult result) - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } + AddIdentityErrors(result); + return Response(model); } - - private Task GetCurrentUserAsync() - { - return _userManager.GetUserAsync(HttpContext.User); - } - - private IActionResult InternalServerError(object value = null) - { - return new ObjectResult(value) { StatusCode = 500 }; - } - - #endregion } } diff --git a/src/Equinox.WebApi/Controllers/ApiController.cs b/src/Equinox.WebApi/Controllers/ApiController.cs index a1a32781..da2106b1 100644 --- a/src/Equinox.WebApi/Controllers/ApiController.cs +++ b/src/Equinox.WebApi/Controllers/ApiController.cs @@ -1,6 +1,8 @@ using Equinox.Domain.Core.Notifications; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Identity; namespace Equinox.WebApi.Controllers { @@ -8,7 +10,7 @@ public abstract class ApiController : ControllerBase { private readonly IDomainNotificationHandler _notifications; - public ApiController(IDomainNotificationHandler notifications) + protected ApiController(IDomainNotificationHandler notifications) { _notifications = notifications; } @@ -19,5 +21,46 @@ protected bool IsValidOperation() { return (!_notifications.HasNotifications()); } + + protected new IActionResult Response(object result = null) + { + if (IsValidOperation()) + { + return Ok(new + { + success = true, + data = result + }); + } + + return BadRequest(new + { + success = false, + errors = _notifications.GetNotifications().Select(n => n.Value) + }); + } + + protected void NotifyModelStateErrors() + { + var erros = ModelState.Values.SelectMany(v => v.Errors); + foreach (var erro in erros) + { + var erroMsg = erro.Exception == null ? erro.ErrorMessage : erro.Exception.Message; + NotifyError(string.Empty, erroMsg); + } + } + + protected void NotifyError(string code, string message) + { + _notifications.Handle(new DomainNotification(code, message)); + } + + protected void AddIdentityErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + NotifyError(result.ToString(), error.Description); + } + } } } diff --git a/src/Equinox.WebApi/Controllers/CustomerController.cs b/src/Equinox.WebApi/Controllers/CustomerController.cs index 20cb7017..c7de6bfd 100644 --- a/src/Equinox.WebApi/Controllers/CustomerController.cs +++ b/src/Equinox.WebApi/Controllers/CustomerController.cs @@ -12,129 +12,79 @@ public class CustomerController : ApiController { private readonly ICustomerAppService _customerAppService; - public CustomerController(ICustomerAppService customerAppService, IDomainNotificationHandler notifications) : base(notifications) + public CustomerController(ICustomerAppService customerAppService, + IDomainNotificationHandler notifications) : base(notifications) { _customerAppService = customerAppService; } [HttpGet] [AllowAnonymous] - [Route("customer-management/list-all")] - public IActionResult Index() + [Route("customer-management")] + public IActionResult Get() { - return Ok(_customerAppService.GetAll()); + return Response(_customerAppService.GetAll()); } [HttpGet] [AllowAnonymous] - [Route("customer-management/customer-details/{id:guid}")] - public IActionResult Details(Guid? id) + [Route("customer-management/{id:guid}")] + public IActionResult Get(Guid id) { - if (id == null) - { - return BadRequest(); - } - - var customerViewModel = _customerAppService.GetById(id.Value); + var customerViewModel = _customerAppService.GetById(id); - if (customerViewModel == null) - { - return NotFound(); - } - - return Ok(customerViewModel); + return Response(customerViewModel); } [HttpPost] [Authorize(Policy = "CanWriteCustomerData")] - [Route("customer-management/register-new")] - [ValidateAntiForgeryToken] - public IActionResult Create(CustomerViewModel customerViewModel) - { - if (!ModelState.IsValid) return BadRequest(customerViewModel); - _customerAppService.Register(customerViewModel); - - if (IsValidOperation()) - Ok(); - - return BadRequest(Notifications); - } - - [HttpGet] - [Authorize(Policy = "CanWriteCustomerData")] - [Route("customer-management/edit-customer/{id:guid}")] - public IActionResult Edit(Guid? id) + [Route("customer-management")] + public IActionResult Post([FromBody]CustomerViewModel customerViewModel) { - if (id == null) + if (!ModelState.IsValid) { - return NotFound(); + NotifyModelStateErrors(); + return Response(customerViewModel); } - var customerViewModel = _customerAppService.GetById(id.Value); - - if (customerViewModel == null) - { - return NotFound(); - } + _customerAppService.Register(customerViewModel); - return Ok(customerViewModel); + return Response(customerViewModel); } - + [HttpPut] [Authorize(Policy = "CanWriteCustomerData")] - [Route("customer-management/edit-customer/{id:guid}")] - [ValidateAntiForgeryToken] - public IActionResult Edit(CustomerViewModel customerViewModel) - { - if (!ModelState.IsValid) return BadRequest(customerViewModel); - - _customerAppService.Update(customerViewModel); - - if (IsValidOperation()) - Ok(); - - return BadRequest(Notifications); - } - - [HttpGet] - [Authorize(Policy = "CanRemoveCustomerData")] - [Route("customer-management/remove-customer/{id:guid}")] - public IActionResult Delete(Guid? id) + [Route("customer-management")] + public IActionResult Put([FromBody]CustomerViewModel customerViewModel) { - if (id == null) + if (!ModelState.IsValid) { - return BadRequest(); + NotifyModelStateErrors(); + return Response(customerViewModel); } - var customerViewModel = _customerAppService.GetById(id.Value); - - if (customerViewModel == null) - { - return NotFound(); - } + _customerAppService.Update(customerViewModel); - return Ok(customerViewModel); + return Response(customerViewModel); } - [HttpPost, ActionName("Delete")] + [HttpDelete] [Authorize(Policy = "CanRemoveCustomerData")] - [Route("customer-management/remove-customer/{id:guid}")] - [ValidateAntiForgeryToken] - public IActionResult DeleteConfirmed(Guid id) + [Route("customer-management")] + public IActionResult Delete(Guid id) { _customerAppService.Remove(id); - - if (!IsValidOperation()) return BadRequest(Notifications); - - return DeleteConfirmed(id); + + return Response(); } + [HttpGet] [AllowAnonymous] - [Route("customer-management/customer-history/{id:guid}")] + [Route("customer-management/history/{id:guid}")] public IActionResult History(Guid id) { var customerHistoryData = _customerAppService.GetAllHistory(id); - return Ok(customerHistoryData); + return Response(customerHistoryData); } } } diff --git a/src/Equinox.WebApi/Controllers/ManageController.cs b/src/Equinox.WebApi/Controllers/ManageController.cs deleted file mode 100644 index 830650f7..00000000 --- a/src/Equinox.WebApi/Controllers/ManageController.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Equinox.Infra.CrossCutting.Identity.Models; -using Equinox.Infra.CrossCutting.Identity.Models.ManageViewModels; -using Equinox.Infra.CrossCutting.Identity.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace Equinox.WebApi.Controllers -{ - [Authorize] - public class ManageController : Controller - { - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly IEmailSender _emailSender; - private readonly ISmsSender _smsSender; - private readonly ILogger _logger; - - public ManageController( - UserManager userManager, - SignInManager signInManager, - IEmailSender emailSender, - ISmsSender smsSender, - ILoggerFactory loggerFactory) - { - _userManager = userManager; - _signInManager = signInManager; - _emailSender = emailSender; - _smsSender = smsSender; - _logger = loggerFactory.CreateLogger(); - } - - // - // GET: /Manage/Index - [HttpGet] - public async Task Index(ManageMessageId? message = null) - { - ViewData["StatusMessage"] = - message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." - : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." - : message == ManageMessageId.SetTwoFactorSuccess ? "Your two-factor authentication provider has been set." - : message == ManageMessageId.Error ? "An error has occurred." - : message == ManageMessageId.AddPhoneSuccess ? "Your phone number was added." - : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed." - : ""; - - var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } - var model = new IndexViewModel - { - HasPassword = await _userManager.HasPasswordAsync(user), - PhoneNumber = await _userManager.GetPhoneNumberAsync(user), - TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user), - Logins = await _userManager.GetLoginsAsync(user), - BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user) - }; - return View(model); - } - - // - // POST: /Manage/RemoveLogin - [HttpPost] - [ValidateAntiForgeryToken] - public async Task RemoveLogin(RemoveLoginViewModel account) - { - ManageMessageId? message = ManageMessageId.Error; - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.RemoveLoginAsync(user, account.LoginProvider, account.ProviderKey); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - message = ManageMessageId.RemoveLoginSuccess; - } - } - return RedirectToAction(nameof(ManageLogins), new { Message = message }); - } - - // - // GET: /Manage/AddPhoneNumber - public IActionResult AddPhoneNumber() - { - return View(); - } - - // - // POST: /Manage/AddPhoneNumber - [HttpPost] - [ValidateAntiForgeryToken] - public async Task AddPhoneNumber(AddPhoneNumberViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - // Generate the token and send it - var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } - var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, model.PhoneNumber); - await _smsSender.SendSmsAsync(model.PhoneNumber, "Your security code is: " + code); - return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber }); - } - - // - // POST: /Manage/EnableTwoFactorAuthentication - [HttpPost] - [ValidateAntiForgeryToken] - public async Task EnableTwoFactorAuthentication() - { - var user = await GetCurrentUserAsync(); - if (user != null) - { - await _userManager.SetTwoFactorEnabledAsync(user, true); - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(1, "User enabled two-factor authentication."); - } - return RedirectToAction(nameof(Index), "Manage"); - } - - // - // POST: /Manage/DisableTwoFactorAuthentication - [HttpPost] - [ValidateAntiForgeryToken] - public async Task DisableTwoFactorAuthentication() - { - var user = await GetCurrentUserAsync(); - if (user != null) - { - await _userManager.SetTwoFactorEnabledAsync(user, false); - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(2, "User disabled two-factor authentication."); - } - return RedirectToAction(nameof(Index), "Manage"); - } - - // - // GET: /Manage/VerifyPhoneNumber - [HttpGet] - public async Task VerifyPhoneNumber(string phoneNumber) - { - var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } - var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber); - // Send an SMS to verify the phone number - return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); - } - - // - // POST: /Manage/VerifyPhoneNumber - [HttpPost] - [ValidateAntiForgeryToken] - public async Task VerifyPhoneNumber(VerifyPhoneNumberViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.AddPhoneSuccess }); - } - } - // If we got this far, something failed, redisplay the form - ModelState.AddModelError(string.Empty, "Failed to verify phone number"); - return View(model); - } - - // - // POST: /Manage/RemovePhoneNumber - [HttpPost] - [ValidateAntiForgeryToken] - public async Task RemovePhoneNumber() - { - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.SetPhoneNumberAsync(user, null); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.RemovePhoneSuccess }); - } - } - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); - } - - // - // GET: /Manage/ChangePassword - [HttpGet] - public IActionResult ChangePassword() - { - return View(); - } - - // - // POST: /Manage/ChangePassword - [HttpPost] - [ValidateAntiForgeryToken] - public async Task ChangePassword(ChangePasswordViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(3, "User changed their password successfully."); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangePasswordSuccess }); - } - AddErrors(result); - return View(model); - } - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); - } - - // - // GET: /Manage/SetPassword - [HttpGet] - public IActionResult SetPassword() - { - return View(); - } - - // - // POST: /Manage/SetPassword - [HttpPost] - [ValidateAntiForgeryToken] - public async Task SetPassword(SetPasswordViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.AddPasswordAsync(user, model.NewPassword); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetPasswordSuccess }); - } - AddErrors(result); - return View(model); - } - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); - } - - //GET: /Manage/ManageLogins - [HttpGet] - public async Task ManageLogins(ManageMessageId? message = null) - { - ViewData["StatusMessage"] = - message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." - : message == ManageMessageId.AddLoginSuccess ? "The external login was added." - : message == ManageMessageId.Error ? "An error has occurred." - : ""; - var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } - var userLogins = await _userManager.GetLoginsAsync(user); - var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList(); - ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1; - return View(new ManageLoginsViewModel - { - CurrentLogins = userLogins, - OtherLogins = otherLogins - }); - } - - // - // POST: /Manage/LinkLogin - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult LinkLogin(string provider) - { - // Request a redirect to the external login provider to link a login for the current user - var redirectUrl = Url.Action("LinkLoginCallback", "Manage"); - var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); - return Challenge(properties, provider); - } - - // - // GET: /Manage/LinkLoginCallback - [HttpGet] - public async Task LinkLoginCallback() - { - var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } - var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); - if (info == null) - { - return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error }); - } - var result = await _userManager.AddLoginAsync(user, info); - var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error; - return RedirectToAction(nameof(ManageLogins), new { Message = message }); - } - - #region Helpers - - private void AddErrors(IdentityResult result) - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - } - - public enum ManageMessageId - { - AddPhoneSuccess, - AddLoginSuccess, - ChangePasswordSuccess, - SetTwoFactorSuccess, - SetPasswordSuccess, - RemoveLoginSuccess, - RemovePhoneSuccess, - Error - } - - private Task GetCurrentUserAsync() - { - return _userManager.GetUserAsync(HttpContext.User); - } - - #endregion - } -} diff --git a/src/Equinox.WebApi/Equinox.WebApi.csproj b/src/Equinox.WebApi/Equinox.WebApi.csproj index ce3c20c2..793b8e05 100644 --- a/src/Equinox.WebApi/Equinox.WebApi.csproj +++ b/src/Equinox.WebApi/Equinox.WebApi.csproj @@ -12,12 +12,13 @@ - + + diff --git a/src/Equinox.WebApi/Properties/launchSettings.json b/src/Equinox.WebApi/Properties/launchSettings.json index 221a9c78..fddaf54f 100644 --- a/src/Equinox.WebApi/Properties/launchSettings.json +++ b/src/Equinox.WebApi/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:51750/", + "applicationUrl": "http://localhost:51750/swagger/", "sslPort": 0 } }, @@ -21,7 +21,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:51751" + "applicationUrl": "http://localhost:51750/swagger/" } } } diff --git a/src/Equinox.WebApi/Startup.cs b/src/Equinox.WebApi/Startup.cs index 86e5b5cf..feeb1dd8 100644 --- a/src/Equinox.WebApi/Startup.cs +++ b/src/Equinox.WebApi/Startup.cs @@ -11,7 +11,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Mvc.Formatters; using System.IO; +using Equinox.Infra.CrossCutting.Bus; +using Equinox.WebApi.Configurations; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Swagger; namespace Equinox.WebApi { @@ -34,8 +39,7 @@ public Startup(IHostingEnvironment env) builder.AddEnvironmentVariables(); Configuration = builder.Build(); } - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { services.AddDbContext(options => @@ -46,7 +50,14 @@ public void ConfigureServices(IServiceCollection services) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - services.AddWebApi(); + services.AddMvcCore(options => + { + options.OutputFormatters.Remove(new XmlDataContractSerializerOutputFormatter()); + options.UseCentralRoutePrefix(new RouteAttribute("api/v{version}")); + }) + .AddApiExplorer() + .AddJsonFormatters(); + services.AddAutoMapper(); services.AddAuthorization(options => @@ -55,12 +66,29 @@ public void ConfigureServices(IServiceCollection services) options.AddPolicy("CanRemoveCustomerData", policy => policy.Requirements.Add(new ClaimRequirement("Customers", "Remove"))); }); + services.AddSwaggerGen(s => + { + s.SwaggerDoc("v1", new Info + { + Version = "v1", + Title = "Equinox Project", + Description = "Equinox API Swagger surface", + Contact = new Contact { Name = "Eduardo Pires", Email = "contato@eduardopires.net.br", Url = "http://www.eduardopires.net.br" }, + License = new License { Name = "MIT", Url = "https://github.com/EduardoPires/EquinoxProject/blob/master/LICENSE" } + }); + }); + + services.AddCors(); + // .NET Native DI Abstraction RegisterServices(services); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app, + IHostingEnvironment env, + ILoggerFactory loggerFactory, + IHttpContextAccessor accessor) { loggerFactory.AddConsole(); @@ -69,10 +97,24 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseDeveloperExceptionPage(); } - app.Run(async (context) => + app.UseCors(c => + { + c.AllowAnyHeader(); + c.AllowAnyMethod(); + c.AllowAnyOrigin(); + }); + + app.UseIdentity(); + app.UseMvc(); + + app.UseSwagger(); + app.UseSwaggerUI(s => { - await context.Response.WriteAsync("Hello World!"); + s.SwaggerEndpoint("/swagger/v1/swagger.json", "Equinox Project API v1.1"); }); + + // Setting the IContainer interface for use like service locator for events. + InMemoryBus.ContainerAccessor = () => accessor.HttpContext.RequestServices; } private static void RegisterServices(IServiceCollection services)