From 406bd9a0d119e88a3af9fbdd011b5ec9a3467758 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Wed, 25 Jan 2023 22:47:05 -0600 Subject: [PATCH] Added endpoint for registering a user locally after successful logon from Auth0 --- YABA.API/Controllers/UsersController.cs | 40 ++++++++++++++++++ .../Controllers/WeatherForecastController.cs | 35 ---------------- YABA.API/Extensions/ControllerExtensions.cs | 35 ++++++++++++++++ YABA.API/ViewModels/UserResponse.cs | 20 +++++++++ YABA.API/YABA.API.csproj | 1 + YABA.Common/Attributes/ClaimNameAttribute.cs | 14 +++++++ YABA.Common/Extensions/EnumExtensions.cs | 29 +++++++++++++ YABA.Common/Lookups/ClaimsLookup.cs | 22 ++++++++++ YABA.Common/YABA.Common.csproj | 12 ++++++ YABA.Service/Class1.cs | 9 ---- .../DependencyInjectionConfiguration.cs | 3 +- YABA.Service/DTO/UserDTO.cs | 23 ++++++++++ YABA.Service/Interfaces/IUserService.cs | 13 ++++++ YABA.Service/UserService.cs | 42 +++++++++++++++++++ YABA.Service/YABA.Service.csproj | 6 +++ YABA.sln | 10 ++++- 16 files changed, 267 insertions(+), 47 deletions(-) create mode 100644 YABA.API/Controllers/UsersController.cs delete mode 100644 YABA.API/Controllers/WeatherForecastController.cs create mode 100644 YABA.API/Extensions/ControllerExtensions.cs create mode 100644 YABA.API/ViewModels/UserResponse.cs create mode 100644 YABA.Common/Attributes/ClaimNameAttribute.cs create mode 100644 YABA.Common/Extensions/EnumExtensions.cs create mode 100644 YABA.Common/Lookups/ClaimsLookup.cs create mode 100644 YABA.Common/YABA.Common.csproj delete mode 100644 YABA.Service/Class1.cs create mode 100644 YABA.Service/DTO/UserDTO.cs create mode 100644 YABA.Service/Interfaces/IUserService.cs create mode 100644 YABA.Service/UserService.cs diff --git a/YABA.API/Controllers/UsersController.cs b/YABA.API/Controllers/UsersController.cs new file mode 100644 index 0000000..7625d6a --- /dev/null +++ b/YABA.API/Controllers/UsersController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Net; +using YABA.API.Extensions; +using YABA.API.ViewModels; +using YABA.Service.Interfaces; + +namespace YABA.API.Controllers +{ + [ApiVersion("1")] + [Authorize, Route("api/v{version:apiVersion}/[controller]")] + public class UsersController : ControllerBase + { + private readonly IUserService _userService; + + public UsersController(IUserService userService) + { + _userService = userService; + } + + [HttpPost("Register")] + [ProducesResponseType(typeof(UserResponse), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public IActionResult Register() + { + var authProviderId = this.GetAuthProviderId(); + + if (string.IsNullOrEmpty(authProviderId)) return NotFound(); + + var isRegistered = _userService.IsUserRegistered(authProviderId); + + if (isRegistered) return NoContent(); + + var registedUser = _userService.RegisterUser(authProviderId); + return Ok(new UserResponse(registedUser)); + } + } +} diff --git a/YABA.API/Controllers/WeatherForecastController.cs b/YABA.API/Controllers/WeatherForecastController.cs deleted file mode 100644 index f885e88..0000000 --- a/YABA.API/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace YABA.API.Controllers -{ - [ApiController] - [ApiVersion("1")] - [Authorize, Route("api/v{version:apiVersion}/[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(); - } - } -} \ No newline at end of file diff --git a/YABA.API/Extensions/ControllerExtensions.cs b/YABA.API/Extensions/ControllerExtensions.cs new file mode 100644 index 0000000..1ef4f12 --- /dev/null +++ b/YABA.API/Extensions/ControllerExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using YABA.Common.Extensions; +using YABA.Common.Lookups; + +namespace YABA.API.Extensions +{ + public static class ControllerExtensions + { + public static string GetAuthProviderId(this ControllerBase controller) + { + return GetCustomClaim(controller, ClaimsLookup.AuthProviderId); + } + + public static int GetUserId(this ControllerBase controller) + { + var isValidUserId = int.TryParse(GetCustomClaim(controller, ClaimsLookup.UserId), out int userId); + return isValidUserId ? userId : 0; + } + + public static string GetCustomClaim(this ControllerBase controller, ClaimsLookup claim) + { + var claimsIdentity = controller.User.Identity as ClaimsIdentity; + return claimsIdentity.FindFirst(claim.GetClaimName())?.Value.ToString(); + } + + public static string GetIpAddress(this ControllerBase controller) + { + if (controller.Request.Headers.ContainsKey("X-Forwarded-For")) + return controller.Request.Headers["X-Forwarded-For"]; + + return controller.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(); + } + } +} diff --git a/YABA.API/ViewModels/UserResponse.cs b/YABA.API/ViewModels/UserResponse.cs new file mode 100644 index 0000000..0abae39 --- /dev/null +++ b/YABA.API/ViewModels/UserResponse.cs @@ -0,0 +1,20 @@ +using YABA.Service.DTO; + +namespace YABA.API.ViewModels +{ + public class UserResponse + { + public int Id { get; set; } + public bool IsDeleted { get; set; } + public DateTimeOffset CreatedOn { get; set; } + public DateTimeOffset LastModified { get; set; } + + public UserResponse(UserDTO value) + { + Id = value.Id; + IsDeleted = value.IsDeleted; + CreatedOn = value.CreatedOn; + LastModified = value.LastModified; + } + } +} diff --git a/YABA.API/YABA.API.csproj b/YABA.API/YABA.API.csproj index 4a4e946..8e3caa4 100644 --- a/YABA.API/YABA.API.csproj +++ b/YABA.API/YABA.API.csproj @@ -23,6 +23,7 @@ + diff --git a/YABA.Common/Attributes/ClaimNameAttribute.cs b/YABA.Common/Attributes/ClaimNameAttribute.cs new file mode 100644 index 0000000..20d2190 --- /dev/null +++ b/YABA.Common/Attributes/ClaimNameAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace YABA.Common.Attributes +{ + public class ClaimNameAttribute : Attribute + { + public string Name { get; private set; } + + public ClaimNameAttribute(string name) + { + this.Name = name; + } + } +} diff --git a/YABA.Common/Extensions/EnumExtensions.cs b/YABA.Common/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..b942726 --- /dev/null +++ b/YABA.Common/Extensions/EnumExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using YABA.Common.Attributes; +using YABA.Common.Lookups; + +namespace YABA.Common.Extensions +{ + public static class EnumExtensions + { + public static TAttribute GetAttribute(this Enum value) where TAttribute : Attribute + { + var enumType = value.GetType(); + var name = Enum.GetName(enumType, value); + return enumType.GetField(name).GetCustomAttributes(false).OfType().SingleOrDefault(); + } + + public static string GetDisplayName(this Enum enumValue) + { + return enumValue.GetAttribute().Name; + } + + public static string GetClaimName(this ClaimsLookup claimLookup) + { + return claimLookup.GetAttribute().Name; + } + + } +} diff --git a/YABA.Common/Lookups/ClaimsLookup.cs b/YABA.Common/Lookups/ClaimsLookup.cs new file mode 100644 index 0000000..43972d5 --- /dev/null +++ b/YABA.Common/Lookups/ClaimsLookup.cs @@ -0,0 +1,22 @@ +using YABA.Common.Attributes; + +namespace YABA.Common.Lookups +{ + public enum ClaimsLookup + { + [ClaimNameAttribute("https://dev.iwanaga.moe/api/auth_provider_id")] + AuthProviderId = 1, + + [ClaimNameAttribute("https://dev.iwanaga.moe/api/email_address")] + UserEmail = 2, + + [ClaimNameAttribute("https://dev.iwanaga.moe/api/email_verified")] + IsEmailConfirmed = 3, + + [ClaimNameAttribute("https://dev.iwanaga.moe/api/username")] + Username = 4, + + [ClaimNameAttribute("https://dev.iwanaga.moe/api/id")] + UserId = 5 + } +} diff --git a/YABA.Common/YABA.Common.csproj b/YABA.Common/YABA.Common.csproj new file mode 100644 index 0000000..344117a --- /dev/null +++ b/YABA.Common/YABA.Common.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + enable + + + + + + + diff --git a/YABA.Service/Class1.cs b/YABA.Service/Class1.cs deleted file mode 100644 index 955651f..0000000 --- a/YABA.Service/Class1.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace YABA.Service -{ - public class Class1 - { - - } -} diff --git a/YABA.Service/Configuration/DependencyInjectionConfiguration.cs b/YABA.Service/Configuration/DependencyInjectionConfiguration.cs index 5d88cf4..36bc9a2 100644 --- a/YABA.Service/Configuration/DependencyInjectionConfiguration.cs +++ b/YABA.Service/Configuration/DependencyInjectionConfiguration.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using YABA.Service.Interfaces; namespace YABA.Service.Configuration { @@ -7,7 +8,7 @@ namespace YABA.Service.Configuration { public static void AddServiceProjectDependencyInjectionConfiguration(this IServiceCollection services, IConfiguration configuration) { - + services.AddScoped(); } } } diff --git a/YABA.Service/DTO/UserDTO.cs b/YABA.Service/DTO/UserDTO.cs new file mode 100644 index 0000000..cfb3786 --- /dev/null +++ b/YABA.Service/DTO/UserDTO.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using YABA.Models; + +namespace YABA.Service.DTO +{ + public class UserDTO + { + public int Id { get; set; } + public bool IsDeleted { get; set; } + public DateTimeOffset CreatedOn { get; set; } + public DateTimeOffset LastModified { get; set; } + + public UserDTO(User value) + { + Id = value.Id; + IsDeleted = value.IsDeleted; + CreatedOn = value.CreatedOn; + LastModified = value.LastModified; + } + } +} diff --git a/YABA.Service/Interfaces/IUserService.cs b/YABA.Service/Interfaces/IUserService.cs new file mode 100644 index 0000000..dfe1c29 --- /dev/null +++ b/YABA.Service/Interfaces/IUserService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using YABA.Service.DTO; + +namespace YABA.Service.Interfaces +{ + public interface IUserService + { + public bool IsUserRegistered(string authProviderId); + public UserDTO RegisterUser(string authProviderId); + } +} diff --git a/YABA.Service/UserService.cs b/YABA.Service/UserService.cs new file mode 100644 index 0000000..0e607a8 --- /dev/null +++ b/YABA.Service/UserService.cs @@ -0,0 +1,42 @@ +using System.Linq; +using YABA.Data.Context; +using YABA.Models; +using YABA.Service.DTO; +using YABA.Service.Interfaces; + +namespace YABA.Service +{ + public class UserService : IUserService + { + private readonly YABAReadOnlyContext _roContext; + private readonly YABAReadWriteContext _context; + + public UserService (YABAReadOnlyContext roContext, YABAReadWriteContext context) + { + _roContext = roContext; + _context = context; + } + + public bool IsUserRegistered(string authProviderId) + { + return _roContext.Users.Any(x => x.Auth0Id == authProviderId); + } + + public UserDTO RegisterUser(string authProviderId) + { + if(IsUserRegistered(authProviderId)) + { + var user = _roContext.Users.FirstOrDefault(x => x.Auth0Id == authProviderId); + return new UserDTO(user); + } + + var userToRegister = new User + { + Auth0Id = authProviderId + }; + + var registedUser = _context.Users.Add(userToRegister); + return _context.SaveChanges() > 0 ? new UserDTO(registedUser.Entity) : null; + } + } +} diff --git a/YABA.Service/YABA.Service.csproj b/YABA.Service/YABA.Service.csproj index dcb837b..7785b1b 100644 --- a/YABA.Service/YABA.Service.csproj +++ b/YABA.Service/YABA.Service.csproj @@ -10,4 +10,10 @@ + + + + + + diff --git a/YABA.sln b/YABA.sln index 30fcfc9..66e3f25 100644 --- a/YABA.sln +++ b/YABA.sln @@ -7,9 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.API", "YABA.API\YABA.A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Models", "YABA.Models\YABA.Models.csproj", "{DDA30925-F844-426B-8B90-3E6E258BD407}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Data", "YABA.Data\YABA.Data.csproj", "{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Data", "YABA.Data\YABA.Data.csproj", "{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Service", "YABA.Service\YABA.Service.csproj", "{0098D0A8-0273-46F1-9FE7-B0409442251A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Service", "YABA.Service\YABA.Service.csproj", "{0098D0A8-0273-46F1-9FE7-B0409442251A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Common", "YABA.Common\YABA.Common.csproj", "{CA107B5D-4B8E-4515-8380-CB474C57F79C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +35,10 @@ Global {0098D0A8-0273-46F1-9FE7-B0409442251A}.Debug|Any CPU.Build.0 = Debug|Any CPU {0098D0A8-0273-46F1-9FE7-B0409442251A}.Release|Any CPU.ActiveCfg = Release|Any CPU {0098D0A8-0273-46F1-9FE7-B0409442251A}.Release|Any CPU.Build.0 = Release|Any CPU + {CA107B5D-4B8E-4515-8380-CB474C57F79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA107B5D-4B8E-4515-8380-CB474C57F79C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE