From 1083b75684bab596684868ca70a93bca4e6c16cd Mon Sep 17 00:00:00 2001 From: Isaiah Clifford Opoku Date: Mon, 9 Dec 2024 00:40:00 +0000 Subject: [PATCH] Add initial setup for DotnetAuth project - Added DotnetAuth.sln and DotnetAuth.csproj targeting .NET 8.0. - Created appsettings.json and appsettings.Development.json for configuration. - Implemented Program.cs to configure services, middleware, and HTTP pipeline. - Added controllers for authentication (AuthController) and weather forecasts (WeatherForecastController). - Created models and services for user management, JWT settings, and error handling. - Set up Entity Framework Core with ApplicationDbContext and initial migration. - Added HTTP request testing file (DotnetAuth.http) and launch settings. - Implemented user service (UserServiceImpl) for registration, login, token management, and CRUD operations. --- DotnetAuth/DotnetAuth.sln | 22 ++ .../DotnetAuth/Controllers/AuthController.cs | 119 +++++++ .../Controllers/WeatherForecastController.cs | 33 ++ .../Domain/Contracts/ErrorResponse.cs | 9 + .../Domain/Contracts/JwtSettings.cs | 10 + .../Contracts/UserRequsetandResponse.cs | 72 ++++ .../Domain/Entities/ApplicationUser.cs | 15 + DotnetAuth/DotnetAuth/DotnetAuth.csproj | 25 ++ DotnetAuth/DotnetAuth/DotnetAuth.http | 6 + .../Exceptions/GlobalExceptionHandler.cs | 43 +++ .../Extensions/ApplicatrionService.cs | 87 +++++ .../Context/ApplicationDbContext.cs | 17 + .../Infrastructure/Mapping/MappingProfile.cs | 17 + .../20241209000518_InitialCreate.Designer.cs | 303 +++++++++++++++++ .../20241209000518_InitialCreate.cs | 231 +++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 300 ++++++++++++++++ DotnetAuth/DotnetAuth/Program.cs | 113 ++++++ .../DotnetAuth/Properties/launchSettings.json | 41 +++ .../DotnetAuth/Service/CurrentUserService.cs | 21 ++ .../DotnetAuth/Service/ICurrentUserService.cs | 8 + .../DotnetAuth/Service/ITokenService.cs | 10 + .../DotnetAuth/Service/IUserServices.cs | 18 + .../DotnetAuth/Service/ToekenServiceImple.cs | 114 +++++++ .../DotnetAuth/Service/UserServiceImpl.cs | 321 ++++++++++++++++++ DotnetAuth/DotnetAuth/WeatherForecast.cs | 13 + .../DotnetAuth/appsettings.Development.json | 22 ++ DotnetAuth/DotnetAuth/appsettings.json | 9 + 27 files changed, 1999 insertions(+) create mode 100644 DotnetAuth/DotnetAuth.sln create mode 100644 DotnetAuth/DotnetAuth/Controllers/AuthController.cs create mode 100644 DotnetAuth/DotnetAuth/Controllers/WeatherForecastController.cs create mode 100644 DotnetAuth/DotnetAuth/Domain/Contracts/ErrorResponse.cs create mode 100644 DotnetAuth/DotnetAuth/Domain/Contracts/JwtSettings.cs create mode 100644 DotnetAuth/DotnetAuth/Domain/Contracts/UserRequsetandResponse.cs create mode 100644 DotnetAuth/DotnetAuth/Domain/Entities/ApplicationUser.cs create mode 100644 DotnetAuth/DotnetAuth/DotnetAuth.csproj create mode 100644 DotnetAuth/DotnetAuth/DotnetAuth.http create mode 100644 DotnetAuth/DotnetAuth/Exceptions/GlobalExceptionHandler.cs create mode 100644 DotnetAuth/DotnetAuth/Extensions/ApplicatrionService.cs create mode 100644 DotnetAuth/DotnetAuth/Infrastructure/Context/ApplicationDbContext.cs create mode 100644 DotnetAuth/DotnetAuth/Infrastructure/Mapping/MappingProfile.cs create mode 100644 DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.Designer.cs create mode 100644 DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.cs create mode 100644 DotnetAuth/DotnetAuth/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 DotnetAuth/DotnetAuth/Program.cs create mode 100644 DotnetAuth/DotnetAuth/Properties/launchSettings.json create mode 100644 DotnetAuth/DotnetAuth/Service/CurrentUserService.cs create mode 100644 DotnetAuth/DotnetAuth/Service/ICurrentUserService.cs create mode 100644 DotnetAuth/DotnetAuth/Service/ITokenService.cs create mode 100644 DotnetAuth/DotnetAuth/Service/IUserServices.cs create mode 100644 DotnetAuth/DotnetAuth/Service/ToekenServiceImple.cs create mode 100644 DotnetAuth/DotnetAuth/Service/UserServiceImpl.cs create mode 100644 DotnetAuth/DotnetAuth/WeatherForecast.cs create mode 100644 DotnetAuth/DotnetAuth/appsettings.Development.json create mode 100644 DotnetAuth/DotnetAuth/appsettings.json diff --git a/DotnetAuth/DotnetAuth.sln b/DotnetAuth/DotnetAuth.sln new file mode 100644 index 0000000..bba9081 --- /dev/null +++ b/DotnetAuth/DotnetAuth.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35506.116 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetAuth", "DotnetAuth\DotnetAuth.csproj", "{0E9423E7-5BC7-4050-A675-B2B9354A7123}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0E9423E7-5BC7-4050-A675-B2B9354A7123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E9423E7-5BC7-4050-A675-B2B9354A7123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E9423E7-5BC7-4050-A675-B2B9354A7123}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E9423E7-5BC7-4050-A675-B2B9354A7123}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/DotnetAuth/DotnetAuth/Controllers/AuthController.cs b/DotnetAuth/DotnetAuth/Controllers/AuthController.cs new file mode 100644 index 0000000..6c4ecb8 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Controllers/AuthController.cs @@ -0,0 +1,119 @@ +using DotnetAuth.Domain.Contracts; +using DotnetAuth.Service; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DotnetAuth.Controllers +{ + /// + /// Controller for handling authentication-related operations. + /// + [Route("api/")] + public class AuthController : ControllerBase + { + private readonly IUserServices _userService; + + /// + /// Initializes a new instance of the class. + /// + /// The user service for managing user-related operations. + public AuthController(IUserServices userService) + { + _userService = userService; + } + + /// + /// Registers a new user. + /// + /// The user registration request. + /// An representing the result of the operation. + [HttpPost("register")] + [AllowAnonymous] + public async Task Register([FromBody] UserRegisterRequest request) + { + var response = await _userService.RegisterAsync(request); + return Ok(response); + } + + /// + /// Logs in a user. + /// + /// The user login request. + /// An representing the result of the operation. + [HttpPost("login")] + [AllowAnonymous] + public async Task Login([FromBody] UserLoginRequest request) + { + var response = await _userService.LoginAsync(request); + return Ok(response); + } + + /// + /// Gets a user by ID. + /// + /// The ID of the user. + /// An representing the result of the operation. + [HttpGet("user/{id}")] + [Authorize] + public async Task GetById(Guid id) + { + var response = await _userService.GetByIdAsync(id); + return Ok(response); + } + + /// + /// Refreshes the access token using the refresh token. + /// + /// The refresh token request. + /// An representing the result of the operation. + [HttpPost("refresh-token")] + [Authorize] + public async Task RefreshToken([FromBody] RefreshTokenRequest request) + { + var response = await _userService.RefreshTokenAsync(request); + return Ok(response); + } + + /// + /// Revokes the refresh token. + /// + /// The refresh token request to be revoked. + /// An representing the result of the operation. + [HttpPost("revoke-refresh-token")] + [Authorize] + public async Task RevokeRefreshToken([FromBody] RefreshTokenRequest request) + { + var response = await _userService.RevokeRefreshToken(request); + if (response != null && response.Message == "Refresh token revoked successfully") + { + return Ok(response); + } + return BadRequest(response); + } + + /// + /// Gets the current user. + /// + /// An representing the result of the operation. + [HttpGet("current-user")] + [Authorize] + public async Task GetCurrentUser() + { + var response = await _userService.GetCurrentUserAsync(); + return Ok(response); + } + + /// + /// Deletes a user. + /// + /// The ID of the user to be deleted. + /// An representing the result of the operation. + [HttpDelete("user/{id}")] + [Authorize] + public async Task Delete(Guid id) + { + await _userService.DeleteAsync(id); + return Ok(); + } + } +} diff --git a/DotnetAuth/DotnetAuth/Controllers/WeatherForecastController.cs b/DotnetAuth/DotnetAuth/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..da05b6b --- /dev/null +++ b/DotnetAuth/DotnetAuth/Controllers/WeatherForecastController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; + +namespace DotnetAuth.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 = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/DotnetAuth/DotnetAuth/Domain/Contracts/ErrorResponse.cs b/DotnetAuth/DotnetAuth/Domain/Contracts/ErrorResponse.cs new file mode 100644 index 0000000..76e2f0a --- /dev/null +++ b/DotnetAuth/DotnetAuth/Domain/Contracts/ErrorResponse.cs @@ -0,0 +1,9 @@ +namespace DotnetAuth.Domain.Contracts +{ + public class ErrorResponse + { + public string Titel { get; set; } + public int StatusCode { get; set; } + public string Message { get; set; } + } +} diff --git a/DotnetAuth/DotnetAuth/Domain/Contracts/JwtSettings.cs b/DotnetAuth/DotnetAuth/Domain/Contracts/JwtSettings.cs new file mode 100644 index 0000000..bb54f3d --- /dev/null +++ b/DotnetAuth/DotnetAuth/Domain/Contracts/JwtSettings.cs @@ -0,0 +1,10 @@ +namespace DotnetAuth.Domain.Contracts +{ + public class JwtSettings + { + public string? Key { get; set; } + public string ValidIssuer { get; set; } + public string ValidAudience { get; set; } + public double Expires { get; set; } + } +} diff --git a/DotnetAuth/DotnetAuth/Domain/Contracts/UserRequsetandResponse.cs b/DotnetAuth/DotnetAuth/Domain/Contracts/UserRequsetandResponse.cs new file mode 100644 index 0000000..722cfd6 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Domain/Contracts/UserRequsetandResponse.cs @@ -0,0 +1,72 @@ +namespace DotnetAuth.Domain.Contracts +{ + + public class UserRegisterRequest + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public string Gender { get; set; } + + } + + + public class UserResponse + { + public Guid Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public string Gender { get; set; } + public DateTime CreateAt { get; set; } + public DateTime UpdateAt { get; set; } + public string? AccessToken { get; set; } + public string? RefreshToken { get; set; } + + + } + + public class UserLoginRequest + { + public string Email { get; set; } + public string Password { get; set; } + } + + public class CurrentUserResponse + { + + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public string Gender { get; set; } + public string AccessToken { get; set; } + public DateTime CreateAt { get; set; } + public DateTime UpdateAt { get; set; } + + } + + + public class UpdateUserRequest + { + public string FirstName { get; set; } + public string LastName { get; set; } + + public string Email { get; set; } + public string Password { get; set; } + public string Gender { get; set; } + } + + + public class RevokeRefreshTokenResponse + { + public string Message { get; set; } + } + + + public class RefreshTokenRequest + { + public string RefreshToken { get; set; } + } + +} \ No newline at end of file diff --git a/DotnetAuth/DotnetAuth/Domain/Entities/ApplicationUser.cs b/DotnetAuth/DotnetAuth/Domain/Entities/ApplicationUser.cs new file mode 100644 index 0000000..9dbee8d --- /dev/null +++ b/DotnetAuth/DotnetAuth/Domain/Entities/ApplicationUser.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Identity; + +namespace DotnetAuth.Domain.Entities +{ + public class ApplicationUser : IdentityUser + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string Gender { get; set; } + public string? RefreshToken { get; set; } + public DateTime? RefreshTokenExpiryTime { get; set; } + public DateTime CreateAt { get; set; } + public DateTime UpdateAt { get; set; } + } +} diff --git a/DotnetAuth/DotnetAuth/DotnetAuth.csproj b/DotnetAuth/DotnetAuth/DotnetAuth.csproj new file mode 100644 index 0000000..9231fc3 --- /dev/null +++ b/DotnetAuth/DotnetAuth/DotnetAuth.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/DotnetAuth/DotnetAuth/DotnetAuth.http b/DotnetAuth/DotnetAuth/DotnetAuth.http new file mode 100644 index 0000000..1d3e87a --- /dev/null +++ b/DotnetAuth/DotnetAuth/DotnetAuth.http @@ -0,0 +1,6 @@ +@DotnetAuth_HostAddress = http://localhost:5130 + +GET {{DotnetAuth_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/DotnetAuth/DotnetAuth/Exceptions/GlobalExceptionHandler.cs b/DotnetAuth/DotnetAuth/Exceptions/GlobalExceptionHandler.cs new file mode 100644 index 0000000..e4c2830 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Exceptions/GlobalExceptionHandler.cs @@ -0,0 +1,43 @@ +using DotnetAuth.Domain.Contracts; +using Microsoft.AspNetCore.Diagnostics; +using System.Net; + +namespace DotnetAuth.Exceptions +{ + public class GlobalExceptionHandler : IExceptionHandler + { + private readonly ILogger _logger; + + public GlobalExceptionHandler(ILogger logger) + { + _logger = logger; + } + + public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) + { + _logger.LogError(exception, exception.Message); + var response = new ErrorResponse + { + Message = exception.Message, + }; + + switch (exception) + { + case BadHttpRequestException: + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.Titel = exception.GetType().Name; + break; + + default: + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.Titel = "Internal Server Error"; + break; + } + + httpContext.Response.StatusCode = response.StatusCode; + await httpContext.Response.WriteAsJsonAsync(response, cancellationToken); + + return true; + } + } +} diff --git a/DotnetAuth/DotnetAuth/Extensions/ApplicatrionService.cs b/DotnetAuth/DotnetAuth/Extensions/ApplicatrionService.cs new file mode 100644 index 0000000..90fd791 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Extensions/ApplicatrionService.cs @@ -0,0 +1,87 @@ +using DotnetAuth.Domain.Contracts; +using DotnetAuth.Infrastructure.Context; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace DotnetAuth.Extensions +{ + public static partial class ApplicatrionService + { + + // Allow any origin, method, and header + public static void ConfigureCors(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); + } + + + public static void ConfigureIdentity(this IServiceCollection services) + { + services.AddIdentityCore(o => + { + o.Password.RequireNonAlphanumeric = false; + o.Password.RequireDigit = true; + o.Password.RequireLowercase = true; + o.Password.RequireUppercase = false; + o.Password.RequiredLength = 8; + }).AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + } + + + + public static void ConfigureJwt(this IServiceCollection services, IConfiguration configuration) + { + var jwtSettings = configuration.GetSection("JwtSettings").Get(); + if (jwtSettings == null || string.IsNullOrEmpty(jwtSettings.Key)) + + { + throw new InvalidOperationException("JWT secret key is not configured."); + } + + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(o => + { + o.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings.ValidIssuer, + ValidAudience = jwtSettings.ValidAudience, + IssuerSigningKey = secretKey + }; + o.Events = new JwtBearerEvents + { + OnChallenge = context => + { + context.HandleResponse(); + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + var result = System.Text.Json.JsonSerializer.Serialize(new + { + message = "You are not authorized to access this resource. Please authenticate." + }); + return context.Response.WriteAsync(result); + }, + }; + }); + + } + } +} diff --git a/DotnetAuth/DotnetAuth/Infrastructure/Context/ApplicationDbContext.cs b/DotnetAuth/DotnetAuth/Infrastructure/Context/ApplicationDbContext.cs new file mode 100644 index 0000000..f1ac486 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Infrastructure/Context/ApplicationDbContext.cs @@ -0,0 +1,17 @@ +using DotnetAuth.Domain.Entities; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace DotnetAuth.Infrastructure.Context +{ + public class ApplicationDbContext(DbContextOptions options) + : IdentityDbContext(options) + { + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + + } + } +} diff --git a/DotnetAuth/DotnetAuth/Infrastructure/Mapping/MappingProfile.cs b/DotnetAuth/DotnetAuth/Infrastructure/Mapping/MappingProfile.cs new file mode 100644 index 0000000..1e0edec --- /dev/null +++ b/DotnetAuth/DotnetAuth/Infrastructure/Mapping/MappingProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using DotnetAuth.Domain.Contracts; +using DotnetAuth.Domain.Entities; + +namespace DotnetAuth.Infrastructure.Mapping +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + + } + } +} diff --git a/DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.Designer.cs b/DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.Designer.cs new file mode 100644 index 0000000..62dc596 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.Designer.cs @@ -0,0 +1,303 @@ +// +using System; +using DotnetAuth.Infrastructure.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DotnetAuth.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20241209000518_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DotnetAuth.Domain.Entities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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("RefreshToken") + .HasColumnType("nvarchar(max)"); + + b.Property("RefreshTokenExpiryTime") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdateAt") + .HasColumnType("datetime2"); + + 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.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")); + + 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.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + 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") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + 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") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + 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("DotnetAuth.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("DotnetAuth.Domain.Entities.ApplicationUser", 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("DotnetAuth.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("DotnetAuth.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.cs b/DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.cs new file mode 100644 index 0000000..84fefad --- /dev/null +++ b/DotnetAuth/DotnetAuth/Migrations/20241209000518_InitialCreate.cs @@ -0,0 +1,231 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DotnetAuth.Migrations +{ + /// + public partial class InitialCreate : 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), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + Gender = table.Column(type: "nvarchar(max)", nullable: false), + RefreshToken = table.Column(type: "nvarchar(max)", nullable: true), + RefreshTokenExpiryTime = table.Column(type: "datetime2", nullable: true), + CreateAt = table.Column(type: "datetime2", nullable: false), + UpdateAt = table.Column(type: "datetime2", 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(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", 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(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", 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/DotnetAuth/DotnetAuth/Migrations/ApplicationDbContextModelSnapshot.cs b/DotnetAuth/DotnetAuth/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..b5bff0b --- /dev/null +++ b/DotnetAuth/DotnetAuth/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,300 @@ +// +using System; +using DotnetAuth.Infrastructure.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DotnetAuth.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DotnetAuth.Domain.Entities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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("RefreshToken") + .HasColumnType("nvarchar(max)"); + + b.Property("RefreshTokenExpiryTime") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdateAt") + .HasColumnType("datetime2"); + + 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.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")); + + 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.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + 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") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + 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") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + 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("DotnetAuth.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("DotnetAuth.Domain.Entities.ApplicationUser", 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("DotnetAuth.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("DotnetAuth.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DotnetAuth/DotnetAuth/Program.cs b/DotnetAuth/DotnetAuth/Program.cs new file mode 100644 index 0000000..a1278fd --- /dev/null +++ b/DotnetAuth/DotnetAuth/Program.cs @@ -0,0 +1,113 @@ +using DotnetAuth.Domain.Entities; +using DotnetAuth.Exceptions; +using DotnetAuth.Extensions; +using DotnetAuth.Infrastructure.Context; +using DotnetAuth.Infrastructure.Mapping; +using DotnetAuth.Service; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + + +builder.Services.AddHttpContextAccessor(); + + +builder.Services.AddExceptionHandler(); + +builder.Services.AddProblemDetails(); + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); + + + +// Adding Swagger +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "User Auth", Version = "v1", Description = "Services to Authenticate user" }); + + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "Please enter a valid token in the following format: {your token here} do not add the word 'Bearer' before it." + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); +}); + + + +// Adding Database context +builder.Services.AddDbContext(options => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("sqlConnection")); +}); + +// Adding Identity + +builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + +// Adding Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + + +// Regsitering AutoMapper +builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly); + + +// Adding Jwt from extension method +builder.Services.ConfigureIdentity(); +builder.Services.ConfigureJwt(builder.Configuration); +builder.Services.ConfigureCors(); + + + +var app = builder.Build(); +app.UseCors("CorsPolicy"); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseExceptionHandler(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/DotnetAuth/DotnetAuth/Properties/launchSettings.json b/DotnetAuth/DotnetAuth/Properties/launchSettings.json new file mode 100644 index 0000000..a0e1958 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:15112", + "sslPort": 44398 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7139;http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/DotnetAuth/DotnetAuth/Service/CurrentUserService.cs b/DotnetAuth/DotnetAuth/Service/CurrentUserService.cs new file mode 100644 index 0000000..3851812 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Service/CurrentUserService.cs @@ -0,0 +1,21 @@ +using System.Security.Claims; + +namespace DotnetAuth.Service +{ + public class CurrentUserService : ICurrentUserService + { + private readonly IHttpContextAccessor _httpContextAccessor; + public CurrentUserService() { } + + public CurrentUserService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string? GetUserId() + { + var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier); + return userId; + } + } +} diff --git a/DotnetAuth/DotnetAuth/Service/ICurrentUserService.cs b/DotnetAuth/DotnetAuth/Service/ICurrentUserService.cs new file mode 100644 index 0000000..a895808 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Service/ICurrentUserService.cs @@ -0,0 +1,8 @@ +namespace DotnetAuth.Service +{ + public interface ICurrentUserService + { + public string? GetUserId(); + + } +} diff --git a/DotnetAuth/DotnetAuth/Service/ITokenService.cs b/DotnetAuth/DotnetAuth/Service/ITokenService.cs new file mode 100644 index 0000000..d6fbe25 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Service/ITokenService.cs @@ -0,0 +1,10 @@ +using DotnetAuth.Domain.Entities; + +namespace DotnetAuth.Service +{ + public interface ITokenService + { + Task GenerateToken(ApplicationUser user); + string GenerateRefreshToken(); + } +} diff --git a/DotnetAuth/DotnetAuth/Service/IUserServices.cs b/DotnetAuth/DotnetAuth/Service/IUserServices.cs new file mode 100644 index 0000000..1c93db4 --- /dev/null +++ b/DotnetAuth/DotnetAuth/Service/IUserServices.cs @@ -0,0 +1,18 @@ +using DotnetAuth.Domain.Contracts; +namespace DotnetAuth.Service +{ + public interface IUserServices + { + + + Task RegisterAsync(UserRegisterRequest request); + Task GetCurrentUserAsync(); + Task GetByIdAsync(Guid id); + Task UpdateAsync(Guid id, UpdateUserRequest request); + Task DeleteAsync(Guid id); + Task RevokeRefreshToken(RefreshTokenRequest refreshTokenRemoveRequest); + Task RefreshTokenAsync(RefreshTokenRequest request); + + Task LoginAsync(UserLoginRequest request); + } +} diff --git a/DotnetAuth/DotnetAuth/Service/ToekenServiceImple.cs b/DotnetAuth/DotnetAuth/Service/ToekenServiceImple.cs new file mode 100644 index 0000000..dcf200a --- /dev/null +++ b/DotnetAuth/DotnetAuth/Service/ToekenServiceImple.cs @@ -0,0 +1,114 @@ +using DotnetAuth.Domain.Contracts; +using DotnetAuth.Domain.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; + +namespace DotnetAuth.Service +{ + /// + /// Implementation of the ITokenService interface for generating JWT tokens and refresh tokens. + /// + public class ToekenServiceImple : ITokenService + { + private readonly SymmetricSecurityKey _secretKey; + private readonly string? _validIssuer; + private readonly string? _validAudience; + private readonly double _expires; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration settings. + /// The user manager for managing user information. + /// The logger for logging information. + /// Thrown when JWT secret key is not configured. + public ToekenServiceImple(IConfiguration configuration, UserManager userManager, ILogger logger) + { + _userManager = userManager; + _logger = logger; + var jwtSettings = configuration.GetSection("JwtSettings").Get(); + if (jwtSettings == null || string.IsNullOrEmpty(jwtSettings.Key)) + { + throw new InvalidOperationException("JWT secret key is not configured."); + } + + _secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); + _validIssuer = jwtSettings.ValidIssuer; + _validAudience = jwtSettings.ValidAudience; + _expires = jwtSettings.Expires; + } + + /// + /// Generates a JWT token for the specified user. + /// + /// The user for whom the token is generated. + /// A task that represents the asynchronous operation. The task result contains the generated JWT token. + public async Task GenerateToken(ApplicationUser user) + { + var singingCredentials = new SigningCredentials(_secretKey, SecurityAlgorithms.HmacSha256); + var claims = await GetClaimsAsync(user); + var tokenOptions = GenerateTokenOptions(singingCredentials, claims); + return new JwtSecurityTokenHandler().WriteToken(tokenOptions); + } + + /// + /// Gets the claims for the specified user. + /// + /// The user for whom the claims are retrieved. + /// A task that represents the asynchronous operation. The task result contains the list of claims. + private async Task> GetClaimsAsync(ApplicationUser user) + { + var claims = new List + { + new Claim(ClaimTypes.Name, user?.UserName ?? string.Empty), + new Claim(ClaimTypes.NameIdentifier, user?.Id ?? string.Empty), + new Claim(ClaimTypes.Email, user?.Email ?? string.Empty), + new Claim("FirstName", user?.FirstName ?? string.Empty), + new Claim("LastName", user?.LastName ?? string.Empty), + new Claim("Gender", user?.Gender ?? string.Empty) + }; + + var roles = await _userManager.GetRolesAsync(user); + claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + + return claims; + } + + /// + /// Generates the token options for the JWT token. + /// + /// The signing credentials for the token. + /// The claims to be included in the token. + /// The generated JWT token options. + private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List claims) + { + return new JwtSecurityToken( + issuer: _validIssuer, + audience: _validAudience, + claims: claims, + expires: DateTime.Now.AddMinutes(_expires), + signingCredentials: signingCredentials + ); + } + + /// + /// Generates a refresh token. + /// + /// The generated refresh token. + public string GenerateRefreshToken() + { + var randomNumber = new byte[64]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomNumber); + + var refreshToken = Convert.ToBase64String(randomNumber); + return refreshToken; + } + } +} diff --git a/DotnetAuth/DotnetAuth/Service/UserServiceImpl.cs b/DotnetAuth/DotnetAuth/Service/UserServiceImpl.cs new file mode 100644 index 0000000..192081e --- /dev/null +++ b/DotnetAuth/DotnetAuth/Service/UserServiceImpl.cs @@ -0,0 +1,321 @@ +using AutoMapper; +using DotnetAuth.Domain.Contracts; +using DotnetAuth.Domain.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using System.Security.Cryptography; +using System.Text; + +namespace DotnetAuth.Service +{ + /// + /// Implementation of the IUserServices interface for managing user-related operations. + /// + public class UserServiceImpl : IUserServices + { + private readonly ITokenService _tokenService; + private readonly ICurrentUserService _currentUserService; + private readonly UserManager _userManager; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The token service for generating tokens. + /// The current user service for retrieving current user information. + /// The user manager for managing user information. + /// The mapper for mapping objects. + /// The logger for logging information. + public UserServiceImpl(ITokenService tokenService, ICurrentUserService currentUserService, UserManager userManager, IMapper mapper, ILogger logger) + { + _tokenService = tokenService; + _currentUserService = currentUserService; + _userManager = userManager; + _mapper = mapper; + _logger = logger; + } + + /// + /// Registers a new user. + /// + /// The user registration request. + /// A task that represents the asynchronous operation. The task result contains the user response. + /// Thrown when the email already exists or user creation fails. + public async Task RegisterAsync(UserRegisterRequest request) + { + _logger.LogInformation("Registering user"); + var existingUser = await _userManager.FindByEmailAsync(request.Email); + if (existingUser != null) + { + _logger.LogError("Email already exists"); + throw new Exception("Email already exists"); + } + + var newUser = _mapper.Map(request); + + // Generate a unique username + newUser.UserName = GenerateUserName(request.FirstName, request.LastName); + var result = await _userManager.CreateAsync(newUser, request.Password); + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + _logger.LogError("Failed to create user: {errors}", errors); + throw new Exception($"Failed to create user: {errors}"); + } + _logger.LogInformation("User created successfully"); + await _tokenService.GenerateToken(newUser); + newUser.CreateAt = DateTime.Now; + newUser.UpdateAt = DateTime.Now; + return _mapper.Map(newUser); + } + + /// + /// Generates a unique username by concatenating the first name and last name. + /// + /// The first name of the user. + /// The last name of the user. + /// The generated unique username. + private string GenerateUserName(string firstName, string lastName) + { + var baseUsername = $"{firstName}{lastName}".ToLower(); + + // Check if the username already exists + var username = baseUsername; + var count = 1; + while (_userManager.Users.Any(u => u.UserName == username)) + { + username = $"{baseUsername}{count}"; + count++; + } + return username; + } + + /// + /// Logs in a user. + /// + /// The user login request. + /// A task that represents the asynchronous operation. The task result contains the user response. + /// Thrown when the login request is null. + /// Thrown when the email or password is invalid or user update fails. + public async Task LoginAsync(UserLoginRequest request) + { + if (request == null) + { + _logger.LogError("Login request is null"); + throw new ArgumentNullException(nameof(request)); + } + + var user = await _userManager.FindByEmailAsync(request.Email); + if (user == null || !await _userManager.CheckPasswordAsync(user, request.Password)) + { + _logger.LogError("Invalid email or password"); + throw new Exception("Invalid email or password"); + } + + // Generate access token + var token = await _tokenService.GenerateToken(user); + + // Generate refresh token + var refreshToken = _tokenService.GenerateRefreshToken(); + + // Hash the refresh token and store it in the database or override the existing refresh token + using var sha256 = SHA256.Create(); + var refreshTokenHash = sha256.ComputeHash(Encoding.UTF8.GetBytes(refreshToken)); + user.RefreshToken = Convert.ToBase64String(refreshTokenHash); + user.RefreshTokenExpiryTime = DateTime.Now.AddDays(2); + + user.CreateAt = DateTime.Now; + + // Update user information in database + var result = await _userManager.UpdateAsync(user); + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + _logger.LogError("Failed to update user: {errors}", errors); + throw new Exception($"Failed to update user: {errors}"); + } + + var userResponse = _mapper.Map(user); + userResponse.AccessToken = token; + userResponse.RefreshToken = refreshToken; + + return userResponse; + } + + /// + /// Gets a user by ID. + /// + /// The ID of the user. + /// A task that represents the asynchronous operation. The task result contains the user response. + /// Thrown when the user is not found. + public async Task GetByIdAsync(Guid id) + { + _logger.LogInformation("Getting user by id"); + var user = await _userManager.FindByIdAsync(id.ToString()); + if (user == null) + { + _logger.LogError("User not found"); + throw new Exception("User not found"); + } + _logger.LogInformation("User found"); + return _mapper.Map(user); + } + + /// + /// Gets the current user. + /// + /// A task that represents the asynchronous operation. The task result contains the current user response. + /// Thrown when the user is not found. + public async Task GetCurrentUserAsync() + { + var user = await _userManager.FindByIdAsync(_currentUserService.GetUserId()); + if (user == null) + { + _logger.LogError("User not found"); + throw new Exception("User not found"); + } + return _mapper.Map(user); + } + + /// + /// Refreshes the access token using the refresh token. + /// + /// The refresh token request. + /// A task that represents the asynchronous operation. The task result contains the current user response. + /// Thrown when the refresh token is invalid or expired. + public async Task RefreshTokenAsync(RefreshTokenRequest request) + { + _logger.LogInformation("RefreshToken"); + + // Hash the incoming RefreshToken and compare it with the one stored in the database + using var sha256 = SHA256.Create(); + var refreshTokenHash = sha256.ComputeHash(Encoding.UTF8.GetBytes(request.RefreshToken)); + var hashedRefreshToken = Convert.ToBase64String(refreshTokenHash); + + // Find user based on the refresh token + var user = await _userManager.Users.FirstOrDefaultAsync(u => u.RefreshToken == hashedRefreshToken); + if (user == null) + { + _logger.LogError("Invalid refresh token"); + throw new Exception("Invalid refresh token"); + } + + // Validate the refresh token expiry time + if (user.RefreshTokenExpiryTime < DateTime.Now) + { + _logger.LogWarning("Refresh token expired for user ID: {UserId}", user.Id); + throw new Exception("Refresh token expired"); + } + + // Generate a new access token + var newAccessToken = await _tokenService.GenerateToken(user); + _logger.LogInformation("Access token generated successfully"); + var currentUserResponse = _mapper.Map(user); + currentUserResponse.AccessToken = newAccessToken; + return currentUserResponse; + } + + /// + /// Revokes the refresh token. + /// + /// The refresh token request to be revoked. + /// A task that represents the asynchronous operation. The task result contains the revoke refresh token response. + /// Thrown when the refresh token is invalid or expired. + public async Task RevokeRefreshToken(RefreshTokenRequest refreshTokenRemoveRequest) + { + _logger.LogInformation("Revoking refresh token"); + + try + { + // Hash the refresh token + using var sha256 = SHA256.Create(); + var refreshTokenHash = sha256.ComputeHash(Encoding.UTF8.GetBytes(refreshTokenRemoveRequest.RefreshToken)); + var hashedRefreshToken = Convert.ToBase64String(refreshTokenHash); + + // Find the user based on the refresh token + var user = await _userManager.Users.FirstOrDefaultAsync(u => u.RefreshToken == hashedRefreshToken); + if (user == null) + { + _logger.LogError("Invalid refresh token"); + throw new Exception("Invalid refresh token"); + } + + // Validate the refresh token expiry time + if (user.RefreshTokenExpiryTime < DateTime.Now) + { + _logger.LogWarning("Refresh token expired for user ID: {UserId}", user.Id); + throw new Exception("Refresh token expired"); + } + + // Remove the refresh token + user.RefreshToken = null; + user.RefreshTokenExpiryTime = null; + + // Update user information in database + var result = await _userManager.UpdateAsync(user); + if (!result.Succeeded) + { + _logger.LogError("Failed to update user"); + return new RevokeRefreshTokenResponse + { + Message = "Failed to revoke refresh token" + }; + } + _logger.LogInformation("Refresh token revoked successfully"); + return new RevokeRefreshTokenResponse + { + Message = "Refresh token revoked successfully" + }; + } + catch (Exception ex) + { + _logger.LogError("Failed to revoke refresh token: {ex}", ex.Message); + throw new Exception("Failed to revoke refresh token"); + } + } + + /// + /// Updates a user. + /// + /// The ID of the user to be updated. + /// The update user request. + /// A task that represents the asynchronous operation. The task result contains the user response. + /// Thrown when the user is not found. + public async Task UpdateAsync(Guid id, UpdateUserRequest request) + { + var user = await _userManager.FindByIdAsync(id.ToString()); + if (user == null) + { + _logger.LogError("User not found"); + throw new Exception("User not found"); + } + + user.UpdateAt = DateTime.Now; + user.FirstName = request.FirstName; + user.LastName = request.LastName; + user.Email = request.Email; + user.Gender = request.Gender; + + await _userManager.UpdateAsync(user); + return _mapper.Map(user); + } + + /// + /// Deletes a user. + /// + /// The ID of the user to be deleted. + /// A task that represents the asynchronous operation. + /// Thrown when the user is not found. + public async Task DeleteAsync(Guid id) + { + var user = await _userManager.FindByIdAsync(id.ToString()); + if (user == null) + { + _logger.LogError("User not found"); + throw new Exception("User not found"); + } + await _userManager.DeleteAsync(user); + } + } +} diff --git a/DotnetAuth/DotnetAuth/WeatherForecast.cs b/DotnetAuth/DotnetAuth/WeatherForecast.cs new file mode 100644 index 0000000..aaa2e01 --- /dev/null +++ b/DotnetAuth/DotnetAuth/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace DotnetAuth +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/DotnetAuth/DotnetAuth/appsettings.Development.json b/DotnetAuth/DotnetAuth/appsettings.Development.json new file mode 100644 index 0000000..3927eab --- /dev/null +++ b/DotnetAuth/DotnetAuth/appsettings.Development.json @@ -0,0 +1,22 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + + + "ConnectionStrings": { + "sqlConnection": "Server=localhost;Database=auth_db_pro;Integrated Security=true;TrustServerCertificate=true;" + }, + + + "JwtSettings": { + "validIssuer": "RealWordAPI", + "validAudience": "https://localhost:5052", + "expires": 120, + "key": "ThisIsA32CharactersLongSecretKey!" + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/DotnetAuth/DotnetAuth/appsettings.json b/DotnetAuth/DotnetAuth/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/DotnetAuth/DotnetAuth/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}