diff --git a/.editorconfig b/.editorconfig
index cd17d141f..b84780595 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -16,7 +16,7 @@ indent_size = 2
# Code files
[*.{cs,csx,vb,vbx}]
-indent_size =2
+indent_size = 2
insert_final_newline = true
charset = utf-8-bom
###############################
@@ -61,18 +61,20 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
-dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
-tab_width=2
+tab_width= 2
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
-dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
+dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+end_of_line = crlf
###############################
# C# Coding Conventions #
###############################
@@ -134,6 +136,12 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
###############################
# VB Coding Conventions #
###############################
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7f75a8875..a21e8efed 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -18,12 +18,15 @@
+
+
+
@@ -34,6 +37,7 @@
+
diff --git a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj
index 27e3650be..6dd6af0d0 100644
--- a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj
+++ b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj
@@ -10,9 +10,12 @@
+
+
+
diff --git a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs
index 6de13cae8..37fa3ccfe 100644
--- a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs
+++ b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs
@@ -20,7 +20,7 @@ public AppDbContext(DbContextOptions options,
public DbSet ToDoItems => Set();
public DbSet Projects => Set();
- public DbSet Contributors => Set();
+ public DbSet Contributors => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
diff --git a/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs b/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs
index bc5756c44..5ca135af2 100644
--- a/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs
+++ b/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs
@@ -3,6 +3,7 @@
using Clean.Architecture.Core.Interfaces;
using Clean.Architecture.Core.ProjectAggregate;
using Clean.Architecture.Infrastructure.Data;
+using Clean.Architecture.Infrastructure.Identity.Jwt;
using Clean.Architecture.SharedKernel;
using Clean.Architecture.SharedKernel.Interfaces;
using MediatR;
@@ -54,6 +55,9 @@ protected override void Load(ContainerBuilder builder)
private void RegisterCommonDependencies(ContainerBuilder builder)
{
+ builder.RegisterType()
+ .As().InstancePerLifetimeScope();
+
builder.RegisterGeneric(typeof(EfRepository<>))
.As(typeof(IRepository<>))
.As(typeof(IReadRepository<>))
@@ -69,10 +73,10 @@ private void RegisterCommonDependencies(ContainerBuilder builder)
.As()
.InstancePerLifetimeScope();
+
//builder.Register(context =>
//{
// var c = context.Resolve();
-
// return t => c.Resolve(t);
//});
diff --git a/src/Clean.Architecture.Infrastructure/Identity/AppIdentityDbContext.cs b/src/Clean.Architecture.Infrastructure/Identity/AppIdentityDbContext.cs
new file mode 100644
index 000000000..d2f3f26a5
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Identity/AppIdentityDbContext.cs
@@ -0,0 +1,22 @@
+using System.Reflection.Emit;
+using System.Reflection;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+
+namespace Clean.Architecture.Infrastructure.Identity;
+public class AppIdentityDbContext : IdentityDbContext
+{
+ public AppIdentityDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+ modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
+ // Customize the ASP.NET Identity model and override the defaults if needed.
+ // For example, you can rename the ASP.NET Identity table names and more.
+ // Add your customizations after calling base.OnModelCreating(builder);
+ }
+}
diff --git a/src/Clean.Architecture.Infrastructure/Identity/Jwt/AccessToken.cs b/src/Clean.Architecture.Infrastructure/Identity/Jwt/AccessToken.cs
new file mode 100644
index 000000000..9ddbec867
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Identity/Jwt/AccessToken.cs
@@ -0,0 +1,16 @@
+using System.IdentityModel.Tokens.Jwt;
+
+namespace Clean.Architecture.Infrastructure.Identity.Jwt;
+public class AccessToken
+{
+ public string Access_Token { get; set; }
+ public string TokenType { get; set; }
+ public int ExpiresIn { get; set; }
+
+ public AccessToken(JwtSecurityToken securityToken)
+ {
+ Access_Token = new JwtSecurityTokenHandler().WriteToken(securityToken);
+ TokenType = "Bearer";
+ ExpiresIn = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds;
+ }
+}
diff --git a/src/Clean.Architecture.Infrastructure/Identity/Jwt/IJwtService.cs b/src/Clean.Architecture.Infrastructure/Identity/Jwt/IJwtService.cs
new file mode 100644
index 000000000..590d12d1c
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Identity/Jwt/IJwtService.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Clean.Architecture.Infrastructure.Identity.Jwt;
+public interface IJwtService
+{
+ Task GenerateAsync(User user);
+ int? ValidateJwtAccessTokenAsync(string token);
+}
diff --git a/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs b/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs
new file mode 100644
index 000000000..36cf7aa27
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs
@@ -0,0 +1,98 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Clean.Architecture.Infrastructure.Identity.Jwt;
+public class JwtService : IJwtService
+{
+ private readonly SiteSettings _siteSetting;
+ private readonly UserManager _userManager;
+
+ public JwtService(IOptionsSnapshot settings,
+ UserManager userManager)
+ {
+ _siteSetting = settings.Value;
+ _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
+ }
+
+ public async Task GenerateAsync(User user)
+ {
+ var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character
+ var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature);
+
+ //We can use EncryptingCredentials options in SecurityTokenDescriptor and hence our JWT token will not be parsed by jwt.io site and it will only be decrypted only by our code.
+ //Hence you can secure your token and who can see it
+ //var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character
+ //var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256);
+
+ var claims = await GetClaimsAsync(user);
+
+ var descriptor = new SecurityTokenDescriptor
+ {
+ Issuer = _siteSetting.JwtSettings.Issuer,
+ Audience = _siteSetting.JwtSettings.Audience,
+ IssuedAt = DateTime.Now,
+ NotBefore = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.NotBeforeMinutes),
+ Expires = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.ExpirationMinutes),
+ SigningCredentials = signingCredentials,
+ //EncryptingCredentials = encryptingCredentials,
+ Subject = new ClaimsIdentity(claims)
+ };
+
+ var tokenHandler = new JwtSecurityTokenHandler();
+
+ var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor);
+
+ return new AccessToken(securityToken: securityToken);
+ }
+
+ public int? ValidateJwtAccessTokenAsync(string token)
+ {
+ var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character
+
+ //if you are giving a value to EncryptingCredentials while generating a token then uncomment the encryptionKey and TokenDecryptionKey option so token can be parsed while validating.
+ //var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character
+
+ var tokenHandler = new JwtSecurityTokenHandler();
+ try
+ {
+ tokenHandler.ValidateToken(token, new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = new SymmetricSecurityKey(secretKey),
+ //TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey),
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
+ ClockSkew = TimeSpan.Zero
+ }, out var validatedToken);
+
+ var jwtSecurityToken = (JwtSecurityToken)validatedToken;
+ var userId = int.Parse(jwtSecurityToken.Claims.First(claim => claim.Type == "nameid").Value);
+ return userId;
+ }
+ catch
+ {
+ throw;
+ }
+ }
+
+ private async Task> GetClaimsAsync(User user)
+ {
+ var claims = new List();
+ claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
+ claims.Add(new Claim(ClaimTypes.Name, user.UserName!));
+
+ var userRoles = await _userManager.GetRolesAsync(user);
+
+ foreach (var role in userRoles)
+ {
+ claims.Add(new Claim(ClaimTypes.Role, role));
+ }
+
+ return claims;
+ }
+}
diff --git a/src/Clean.Architecture.Infrastructure/Identity/Role.cs b/src/Clean.Architecture.Infrastructure/Identity/Role.cs
new file mode 100644
index 000000000..42bc43637
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Identity/Role.cs
@@ -0,0 +1,8 @@
+using Clean.Architecture.SharedKernel.Interfaces;
+using Microsoft.AspNetCore.Identity;
+
+namespace Clean.Architecture.Infrastructure.Identity;
+public class Role : IdentityRole, IAggregateRoot
+{
+ public string Description { get; set; } = default!;
+}
diff --git a/src/Clean.Architecture.Infrastructure/Identity/SiteSettings.cs b/src/Clean.Architecture.Infrastructure/Identity/SiteSettings.cs
new file mode 100644
index 000000000..4400fb1a8
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Identity/SiteSettings.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Clean.Architecture.Infrastructure.Identity;
+public class SiteSettings
+{
+ public JwtSettings JwtSettings { get; set; } = default!;
+ public IdentitySettings IdentitySettings { get; set; } = default!;
+}
+
+public class IdentitySettings
+{
+ public bool PasswordRequireDigit { get; set; }
+ public int PasswordRequiredLength { get; set; }
+ public bool PasswordRequireNonAlphanumeric { get; set; }
+ public bool PasswordRequireUppercase { get; set; }
+ public bool PasswordRequireLowercase { get; set; }
+ public bool RequireUniqueEmail { get; set; }
+}
+
+public class JwtSettings
+{
+ public string SecretKey { get; set; } = default!;
+ public string EncryptKey { get; set; } = default!;
+ public string Issuer { get; set; } = default!;
+ public string Audience { get; set; } = default!;
+ public int NotBeforeMinutes { get; set; }
+ public int ExpirationMinutes { get; set; }
+ public int RefreshTokenValidityInDays { get; set; }
+}
diff --git a/src/Clean.Architecture.Infrastructure/Identity/User.cs b/src/Clean.Architecture.Infrastructure/Identity/User.cs
new file mode 100644
index 000000000..2af38ce40
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Identity/User.cs
@@ -0,0 +1,27 @@
+using Clean.Architecture.SharedKernel.Interfaces;
+using Microsoft.AspNetCore.Identity;
+
+namespace Clean.Architecture.Infrastructure.Identity;
+public class User : IdentityUser, IAggregateRoot
+{
+ public User()
+ {
+ IsActive = true;
+ }
+
+ public string FullName { get; set; } = default!;
+
+ public int Age { get; set; }
+
+ public GenderType Gender { get; set; }
+
+ public bool IsActive { get; set; }
+
+ public DateTimeOffset? LastLoginDate { get; set; }
+}
+
+public enum GenderType
+{
+ Male = 1,
+ Female = 2
+}
diff --git a/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.Designer.cs b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.Designer.cs
new file mode 100644
index 000000000..2ca764810
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.Designer.cs
@@ -0,0 +1,385 @@
+//
+using System;
+using Clean.Architecture.Infrastructure.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Clean.Architecture.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppIdentityDbContext))]
+ [Migration("20230416152439_IdentityInitialization")]
+ partial class IdentityInitialization
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Contributor");
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Priority")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Project");
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ContributorId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDone")
+ .HasColumnType("bit");
+
+ b.Property("ProjectId")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.ToTable("ToDoItem");
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Infrastructure.Identity.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .IsRequired()
+ .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("Clean.Architecture.Infrastructure.Identity.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("Age")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Gender")
+ .HasColumnType("int");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.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")
+ .HasColumnType("int");
+
+ 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")
+ .HasColumnType("int");
+
+ 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")
+ .HasColumnType("int");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ 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("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b =>
+ {
+ b.HasOne("Clean.Architecture.Core.ProjectAggregate.Project", null)
+ .WithMany("Items")
+ .HasForeignKey("ProjectId");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b =>
+ {
+ b.Navigation("Items");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.cs b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.cs
new file mode 100644
index 000000000..cc1c390ae
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.cs
@@ -0,0 +1,295 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Clean.Architecture.Infrastructure.Migrations
+{
+ ///
+ public partial class IdentityInitialization : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AspNetRoles",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Description = table.Column(type: "nvarchar(max)", 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: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ FullName = table.Column(type: "nvarchar(max)", nullable: false),
+ Age = table.Column(type: "int", nullable: false),
+ Gender = table.Column(type: "int", nullable: false),
+ IsActive = table.Column(type: "bit", nullable: false),
+ LastLoginDate = table.Column(type: "datetimeoffset", nullable: true),
+ 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: "Contributor",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Contributor", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Project",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false),
+ Priority = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Project", 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: "int", 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: "int", 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: "int", 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: "int", nullable: false),
+ RoleId = table.Column(type: "int", 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: "int", 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.CreateTable(
+ name: "ToDoItem",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Title = table.Column(type: "nvarchar(max)", nullable: false),
+ Description = table.Column(type: "nvarchar(max)", nullable: false),
+ ContributorId = table.Column(type: "int", nullable: true),
+ IsDone = table.Column(type: "bit", nullable: false),
+ ProjectId = table.Column(type: "int", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ToDoItem", x => x.Id);
+ table.ForeignKey(
+ name: "FK_ToDoItem_Project_ProjectId",
+ column: x => x.ProjectId,
+ principalTable: "Project",
+ principalColumn: "Id");
+ });
+
+ 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");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ToDoItem_ProjectId",
+ table: "ToDoItem",
+ column: "ProjectId");
+ }
+
+ ///
+ 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: "Contributor");
+
+ migrationBuilder.DropTable(
+ name: "ToDoItem");
+
+ migrationBuilder.DropTable(
+ name: "AspNetRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUsers");
+
+ migrationBuilder.DropTable(
+ name: "Project");
+ }
+ }
+}
diff --git a/src/Clean.Architecture.Infrastructure/Migrations/AppIdentityDbContextModelSnapshot.cs b/src/Clean.Architecture.Infrastructure/Migrations/AppIdentityDbContextModelSnapshot.cs
new file mode 100644
index 000000000..1fbb1d7a4
--- /dev/null
+++ b/src/Clean.Architecture.Infrastructure/Migrations/AppIdentityDbContextModelSnapshot.cs
@@ -0,0 +1,382 @@
+//
+using System;
+using Clean.Architecture.Infrastructure.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Clean.Architecture.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppIdentityDbContext))]
+ partial class AppIdentityDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Contributor");
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Priority")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Project");
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ContributorId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDone")
+ .HasColumnType("bit");
+
+ b.Property("ProjectId")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.ToTable("ToDoItem");
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Infrastructure.Identity.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .IsRequired()
+ .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("Clean.Architecture.Infrastructure.Identity.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("Age")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Gender")
+ .HasColumnType("int");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.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")
+ .HasColumnType("int");
+
+ 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")
+ .HasColumnType("int");
+
+ 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")
+ .HasColumnType("int");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ 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("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b =>
+ {
+ b.HasOne("Clean.Architecture.Core.ProjectAggregate.Project", null)
+ .WithMany("Items")
+ .HasForeignKey("ProjectId");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b =>
+ {
+ b.Navigation("Items");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Clean.Architecture.Infrastructure/StartupSetup.cs b/src/Clean.Architecture.Infrastructure/StartupSetup.cs
index f17deeeaf..5acc09928 100644
--- a/src/Clean.Architecture.Infrastructure/StartupSetup.cs
+++ b/src/Clean.Architecture.Infrastructure/StartupSetup.cs
@@ -1,6 +1,15 @@
-using Clean.Architecture.Infrastructure.Data;
+using System.Security.Claims;
+using System.Text;
+using Clean.Architecture.Infrastructure.Data;
+using Clean.Architecture.SharedKernel.Utilities;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Tokens;
+using System.Text.Json;
+using Clean.Architecture.Infrastructure.Identity;
namespace Clean.Architecture.Infrastructure;
@@ -9,4 +18,136 @@ public static class StartupSetup
public static void AddDbContext(this IServiceCollection services, string connectionString) =>
services.AddDbContext(options =>
options.UseSqlite(connectionString)); // will be created in web project root
+
+ public static void AddCustomJwtAuthentication(this IServiceCollection services, JwtSettings settings)
+ {
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ })
+ .AddJwtBearer(options =>
+ {
+ var secretKey = Encoding.UTF8.GetBytes(settings.SecretKey);
+ //You can uncomment encryptionKey and TokenDecryptionKey fields if you are going to use it
+ //var encryptionKey = Encoding.UTF8.GetBytes(settings.EncryptKey);
+
+ var validationParameters = new TokenValidationParameters
+ {
+ ClockSkew = TimeSpan.Zero, // default: 5 min
+ RequireSignedTokens = true,
+
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = new SymmetricSecurityKey(secretKey),
+
+ RequireExpirationTime = true,
+ ValidateLifetime = true,
+
+ ValidateAudience = true, //default : false
+ ValidAudience = settings.Audience,
+
+ ValidateIssuer = true, //default : false
+ ValidIssuer = settings.Issuer,
+
+ //TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey)
+ };
+
+ options.RequireHttpsMetadata = false;
+ options.SaveToken = true;
+ options.TokenValidationParameters = validationParameters;
+
+ options.Events = new JwtJwtBearerHelperEvents().GetJwtBeareEvents();
+ });
+ }
+
+ public static void AddCustomIdentity(this IServiceCollection services, IdentitySettings settings, string connectionString)
+ {
+ services.AddDbContext(options =>
+ options.UseSqlServer(connectionString));
+ services.AddIdentity(identityOptions =>
+ {
+ //Password Settings
+ identityOptions.Password.RequireDigit = settings.PasswordRequireDigit;
+ identityOptions.Password.RequiredLength = settings.PasswordRequiredLength;
+ identityOptions.Password.RequireNonAlphanumeric = settings.PasswordRequireNonAlphanumeric; //#@!
+ identityOptions.Password.RequireUppercase = settings.PasswordRequireUppercase;
+ identityOptions.Password.RequireLowercase = settings.PasswordRequireLowercase;
+
+ //UserName Settings
+ identityOptions.User.RequireUniqueEmail = settings.RequireUniqueEmail;
+ })
+ .AddEntityFrameworkStores()
+ .AddDefaultTokenProviders();
+ }
}
+///
+/// A helper JwtJwtBearerHelperEvents class which is added to make the code more readable and avoid any complexity
+///
+internal class JwtJwtBearerHelperEvents
+{
+ public JwtBearerEvents GetJwtBeareEvents()
+ {
+ return new JwtBearerEvents
+ {
+ OnAuthenticationFailed = OnAuthenticationFailedJwtBearerEvent(),
+ OnTokenValidated = OnTokenValidatedJwtBearerEvent(),
+ OnChallenge = OnOnChallengeJwtBearerEvent()
+ };
+ }
+ private Func OnAuthenticationFailedJwtBearerEvent()
+ {
+ return (context) =>
+ {
+ if (context.Exception != null)
+ {
+ throw context.Exception;
+ }
+ return Task.CompletedTask;
+ };
+ }
+ private Func OnTokenValidatedJwtBearerEvent()
+ {
+ return async context =>
+ {
+ var signInManager = context.HttpContext.RequestServices.GetRequiredService>();
+
+ var claimsIdentity = context.Principal!.Identity as ClaimsIdentity;
+
+ if (claimsIdentity==null)
+ context.Fail("This token has no claims identity.");
+
+ if (claimsIdentity!.Claims?.Any() != true)
+ context.Fail("This token has no claims.");
+
+ //Get UserId and check if user exists in db
+ var userId = claimsIdentity.GetUserId();
+ if (userId == 0)
+ context.Fail("UserId has no value in claims.");
+ var user = await signInManager.FindByIdAsync(userId.ToString());
+ if (user == null)
+ context.Fail("User not found.");
+
+ if (!user!.IsActive)
+ context.Fail("User is not active.");
+
+ user.LastLoginDate = DateTime.Now;
+ await signInManager.UpdateAsync(user);
+ };
+ }
+ private Func OnOnChallengeJwtBearerEvent()
+ {
+ return context =>
+ {
+ if (context.AuthenticateFailure != null)
+ throw context.AuthenticateFailure;
+ context.HandleResponse();
+ context.Response.StatusCode = 401;
+ context.Response.ContentType = "application/json";
+ var result = JsonSerializer.Serialize(new { message = "You are not Authorized" });
+ return context.Response.WriteAsync(result);
+ };
+ }
+}
+
+
diff --git a/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj b/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj
index 36765a764..04735fcac 100644
--- a/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj
+++ b/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Clean.Architecture.SharedKernel/Utilities/IdentityExtension.cs b/src/Clean.Architecture.SharedKernel/Utilities/IdentityExtension.cs
new file mode 100644
index 000000000..81f1bcb8c
--- /dev/null
+++ b/src/Clean.Architecture.SharedKernel/Utilities/IdentityExtension.cs
@@ -0,0 +1,36 @@
+using System.Globalization;
+using System.Security.Claims;
+using System.Security.Principal;
+
+namespace Clean.Architecture.SharedKernel.Utilities;
+public static class IdentityExtensions
+{
+ public static string? FindFirstValue(this ClaimsIdentity identity, string claimType)
+ {
+ return identity?.FindFirst(claimType)?.Value;
+ }
+
+ public static string? FindFirstValue(this IIdentity identity, string claimType)
+ {
+ var claimsIdentity = identity as ClaimsIdentity;
+ return claimsIdentity?.FindFirstValue(claimType);
+ }
+
+ public static string? GetUserId(this IIdentity identity)
+ {
+ return identity?.FindFirstValue(ClaimTypes.NameIdentifier);
+ }
+
+ public static T? GetUserId(this IIdentity identity) where T : IConvertible
+ {
+ var userId = identity?.GetUserId();
+ return !string.IsNullOrWhiteSpace(userId)
+ ? (T)Convert.ChangeType(userId, typeof(T), CultureInfo.InvariantCulture)
+ : default;
+ }
+
+ public static string? GetUserName(this IIdentity identity)
+ {
+ return identity?.FindFirstValue(ClaimTypes.Name);
+ }
+}
diff --git a/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs b/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs
index c10cb36dd..28dcf6c17 100644
--- a/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs
+++ b/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs
@@ -1,6 +1,7 @@
using Ardalis.ApiEndpoints;
using Clean.Architecture.Core.ProjectAggregate;
using Clean.Architecture.SharedKernel.Interfaces;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
@@ -24,6 +25,7 @@ public Create(IRepository repository)
OperationId = "Project.Create",
Tags = new[] { "ProjectEndpoints" })
]
+ [Authorize]
public override async Task> HandleAsync(
CreateProjectRequest request,
CancellationToken cancellationToken = new())
diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs
new file mode 100644
index 000000000..49e9f12b6
--- /dev/null
+++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs
@@ -0,0 +1,13 @@
+using Microsoft.Build.Framework;
+
+namespace Clean.Architecture.Web.Endpoints.UserEndpoints;
+
+public class LoginUserRequest
+{
+ public const string Route = "/User/Login";
+ [Required]
+ public string Email { get; set; } = default!;
+
+ [Required]
+ public string Password { get; set; } = default!;
+}
diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserResponse.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserResponse.cs
new file mode 100644
index 000000000..dae5570d6
--- /dev/null
+++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserResponse.cs
@@ -0,0 +1,8 @@
+namespace Clean.Architecture.Web.Endpoints.UserEndpoints;
+
+public class LoginUserResponse
+{
+ public string AccessToken { get; set; } = default!;
+ public string TokenType { get; set; } = default!;
+ public int ExpiresIn { get; set; }
+}
diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs
new file mode 100644
index 000000000..650de5af7
--- /dev/null
+++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs
@@ -0,0 +1,49 @@
+using Clean.Architecture.Infrastructure.Identity;
+using Clean.Architecture.Infrastructure.Identity.Jwt;
+using FastEndpoints;
+using Microsoft.AspNetCore.Identity;
+
+namespace Clean.Architecture.Web.Endpoints.UserEndpoints;
+
+public class Login : Endpoint
+{
+ private readonly UserManager _userManager;
+ private readonly IJwtService _jwtService;
+ public Login(UserManager userManager, IJwtService jwtService)
+ {
+ _userManager = userManager;
+ _jwtService = jwtService;
+ }
+
+ public override void Configure()
+ {
+ Post(LoginUserRequest.Route);
+ AllowAnonymous();
+ Options(x => x
+ .WithTags("UserEndpoints"));
+ }
+ public override async Task HandleAsync(
+ LoginUserRequest request,
+ CancellationToken cancellationToken)
+ {
+ if (request is null)
+ ThrowError("Request body is empty");
+ var user = await _userManager.FindByEmailAsync(request.Email); //userName/email can be used to find a unique user
+ if (user == null)
+ ThrowError("username or password is incorrect");
+
+ var isPasswordValid = await _userManager.CheckPasswordAsync(user, request.Password);
+ if (!isPasswordValid)
+ ThrowError("username or password is incorrect");
+
+ var jwt = await _jwtService.GenerateAsync(user);
+ user.LastLoginDate = DateTime.UtcNow;
+ await _userManager.UpdateAsync(user);
+ await SendOkAsync(new LoginUserResponse
+ {
+ AccessToken = jwt.Access_Token,
+ TokenType = jwt.TokenType,
+ ExpiresIn = jwt.ExpiresIn
+ });
+ }
+}
diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs
new file mode 100644
index 000000000..2611571ed
--- /dev/null
+++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel.DataAnnotations;
+using Clean.Architecture.Infrastructure.Identity;
+
+namespace Clean.Architecture.Web.Endpoints.UserEndpoints;
+
+public class SignUpUserRequest
+{
+ public const string Route = "/User/SignUp";
+
+ [Required]
+ public string UserName { get; set; } = default!;
+
+ [Required]
+ public string Email { get; set; } = default!;
+
+
+ [Required]
+ public string Password { get; set; } = default!;
+
+ [Required]
+ public string FullName { get; set; } = default!;
+
+ [Required]
+ public int Age { get; set; }
+
+ [Required]
+ public GenderType Gender { get; set; }
+}
diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs
new file mode 100644
index 000000000..54bc42522
--- /dev/null
+++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs
@@ -0,0 +1,69 @@
+using Clean.Architecture.Infrastructure.Identity;
+using FastEndpoints;
+using Microsoft.AspNetCore.Identity;
+
+namespace Clean.Architecture.Web.Endpoints.UserEndpoints;
+
+public class SignUp : Endpoint
+{
+ private readonly UserManager _userManager;
+ private readonly RoleManager _roleManager;
+ public SignUp(UserManager userManager, RoleManager roleManager)
+ {
+ _userManager = userManager;
+ _roleManager = roleManager;
+ }
+
+ public override void Configure()
+ {
+ Post(SignUpUserRequest.Route);
+ AllowAnonymous();
+ Options(x => x
+ .WithTags("UserEndpoints"));
+ }
+ public override async Task HandleAsync(
+ SignUpUserRequest request,
+ CancellationToken cancellationToken)
+ {
+ if (request is null)
+ ThrowError("Request body is empty");
+ //TODO:CHECK FOR USERNAME EMPTY OR NULL
+ var newUser = new User
+ {
+ Age = request.Age,
+ FullName = request.FullName!,
+ Gender = request.Gender,
+ UserName = request.UserName,
+ Email = request.Email
+ };
+ var createUserResult = await _userManager.CreateAsync(newUser, request.Password);
+ if (!createUserResult.Succeeded) { ThrowValidationErrors(createUserResult.Errors); }
+
+ var role = await _roleManager.FindByNameAsync("Admin");
+ if (role==null)
+ {
+ var addRoleResult = await _roleManager.CreateAsync(new Role
+ {
+ Name = "Admin",
+ Description = "admin role"
+ });
+ if (!addRoleResult.Succeeded) { ThrowValidationErrors(addRoleResult.Errors); }
+ }
+ else
+ {
+ var assignRoleResult = await _userManager.AddToRoleAsync(newUser, role.Name!);
+ if (!assignRoleResult.Succeeded) { ThrowValidationErrors(assignRoleResult.Errors); }
+ }
+
+ await SendNoContentAsync();
+ }
+ private void ThrowValidationErrors(IEnumerable identityErrors)
+ {
+ foreach (var error in identityErrors)
+ {
+ AddError(error.Description, error.Code);
+ }
+ ThrowIfAnyErrors();
+ }
+}
+
diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs
index 5f63398c1..5dec8d0fc 100644
--- a/src/Clean.Architecture.Web/Program.cs
+++ b/src/Clean.Architecture.Web/Program.cs
@@ -10,6 +10,7 @@
using FastEndpoints.ApiExplorer;
using Microsoft.OpenApi.Models;
using Serilog;
+using Clean.Architecture.Infrastructure.Identity;
var builder = WebApplication.CreateBuilder(args);
@@ -37,6 +38,11 @@
c.EnableAnnotations();
c.OperationFilter();
});
+builder.Services.Configure(builder.Configuration.GetSection(nameof(SiteSettings)));
+
+var sitSettings = builder.Configuration.GetSection(nameof(SiteSettings)).Get();
+builder.Services.AddCustomIdentity(sitSettings!.IdentitySettings, connectionString!);
+builder.Services.AddCustomJwtAuthentication(sitSettings!.JwtSettings);
// add list services for diagnostic purposes - see https://github.com/ardalis/AspNetCoreStartupServices
builder.Services.Configure(config =>
@@ -70,6 +76,8 @@
}
app.UseRouting();
app.UseFastEndpoints();
+app.UseAuthentication();
+app.UseAuthorization();
app.UseHttpsRedirection();
app.UseStaticFiles();
diff --git a/src/Clean.Architecture.Web/appsettings.json b/src/Clean.Architecture.Web/appsettings.json
index 7331e3538..6504e9416 100644
--- a/src/Clean.Architecture.Web/appsettings.json
+++ b/src/Clean.Architecture.Web/appsettings.json
@@ -28,5 +28,24 @@
// }
//}
]
+ },
+ "SiteSettings": {
+ "JwtSettings": {
+ "SecretKey": "LongerThan-16Char-SecretKey",
+ "EncryptKey": "16CharEncryptKey",
+ "Issuer": "CleanArchTemplate",
+ "Audience": "CleanArchTemplate",
+ "NotBeforeMinutes": "0",
+ "ExpirationMinutes": "1440",
+ "RefreshTokenValidityInDays": 7
+ },
+ "IdentitySettings": {
+ "PasswordRequireDigit": "true",
+ "PasswordRequiredLength": "6",
+ "PasswordRequireNonAlphanumeric": "false",
+ "PasswordRequireUppercase": "false",
+ "PasswordRequireLowercase": "false",
+ "RequireUniqueEmail": "true"
+ }
}
}