diff --git a/AdvancedAPI.Business/Services/AuthenticationService.cs b/AdvancedAPI.Business/Services/AuthenticationService.cs new file mode 100644 index 0000000..9b7dc56 --- /dev/null +++ b/AdvancedAPI.Business/Services/AuthenticationService.cs @@ -0,0 +1,53 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using AdvancedAPI.Data.Repositories.Interfaces; +using AdvancedAPI.Data.ViewModels.Authentication; +using Business.Services.Interfaces; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; + +namespace Business.Services; + +/// +public class AuthenticationService : IAuthenticationService +{ + private readonly IIdentityRepository _identityRepository; + private readonly IConfiguration _configuration; + + /// + /// Constructor. + /// + public AuthenticationService(IIdentityRepository identityRepository, IConfiguration configuration) + { + _identityRepository = identityRepository; + _configuration = configuration; + } + + /// + public async Task Login(LoginRequestModel requestModel, CancellationToken ct = default) + { + IdentityUser? user = await _identityRepository.GetUser(requestModel.Username); + if (user != null && await _identityRepository.CheckPassword(user, requestModel.Password)) + { + Claim[] authClaims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, user.UserName), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + SymmetricSecurityKey authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); + + JwtSecurityToken token = new JwtSecurityToken( + issuer: _configuration["Jwt:Issuer"], + audience: _configuration["Jwt:Audience"], + expires: DateTime.Now.AddHours(3), + claims: authClaims, + signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)); + + return token; + } + + return null; + } +} diff --git a/AdvancedAPI.Business/Services/Interfaces/IAuthenticationService.cs b/AdvancedAPI.Business/Services/Interfaces/IAuthenticationService.cs new file mode 100644 index 0000000..f975697 --- /dev/null +++ b/AdvancedAPI.Business/Services/Interfaces/IAuthenticationService.cs @@ -0,0 +1,15 @@ +using System.IdentityModel.Tokens.Jwt; +using AdvancedAPI.Data.ViewModels.Authentication; + +namespace Business.Services.Interfaces; + +/// +/// Authentication service. +/// +public interface IAuthenticationService +{ + /// + /// Logs in the user and returns a token. + /// + public Task Login(LoginRequestModel requestModel, CancellationToken ct = default); +} diff --git a/AdvancedAPI.Data/AdvancedAPI.Data.csproj b/AdvancedAPI.Data/AdvancedAPI.Data.csproj index 5e4d88c..274be67 100644 --- a/AdvancedAPI.Data/AdvancedAPI.Data.csproj +++ b/AdvancedAPI.Data/AdvancedAPI.Data.csproj @@ -9,7 +9,13 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/AdvancedAPI.Data/AdvancedApiContext.cs b/AdvancedAPI.Data/AdvancedApiContext.cs index 55e89a7..b0e5e83 100644 --- a/AdvancedAPI.Data/AdvancedApiContext.cs +++ b/AdvancedAPI.Data/AdvancedApiContext.cs @@ -1,4 +1,5 @@ -using AdvancedAPI.Data.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace AdvancedAPI.Data; @@ -6,7 +7,7 @@ namespace AdvancedAPI.Data; /// /// Database context. /// -public class AdvancedApiContext : DbContext +public class AdvancedApiContext : IdentityDbContext { /// /// Constructor. @@ -15,9 +16,4 @@ public AdvancedApiContext(DbContextOptions options) : base(options) { } - - /// - /// DbSet of . - /// - public DbSet Houses { get; set; } } diff --git a/AdvancedAPI.Data/AdvancedApiContextFactory.cs b/AdvancedAPI.Data/AdvancedApiContextFactory.cs new file mode 100644 index 0000000..8823ee6 --- /dev/null +++ b/AdvancedAPI.Data/AdvancedApiContextFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace AdvancedAPI.Data +{ + /// + /// Factory for creating instances at design time for EF Core tooling. + /// + public class AdvancedApiContextFactory : IDesignTimeDbContextFactory + { + /// + /// Creates a new with design-time configuration. + /// + public AdvancedApiContext CreateDbContext(string[] args) + { + IConfigurationRoot? configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); + + return new AdvancedApiContext(optionsBuilder.Options); + } + } +} \ No newline at end of file diff --git a/AdvancedAPI.Data/DbInitializer.cs b/AdvancedAPI.Data/DbInitializer.cs new file mode 100644 index 0000000..b72e251 --- /dev/null +++ b/AdvancedAPI.Data/DbInitializer.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Identity; + +namespace AdvancedAPI.Data +{ + /// + /// Database initializer. + /// + public class DbInitializer + { + /// + /// Initialization of the database. + /// + public static async Task Initialize(IServiceProvider serviceProvider) + { + UserManager userManager = serviceProvider.GetRequiredService>(); + RoleManager roleManager = serviceProvider.GetRequiredService>(); + + // Seed roles + await SeedRoles(roleManager); + + // Seed admin user + await SeedAdminUser(userManager); + } + + /// + /// Seeding roles into the database. + /// + private static async Task SeedRoles(RoleManager roleManager) + { + string[] roleNames = { "Admin", "User" }; + + foreach (string roleName in roleNames) + { + bool roleExist = await roleManager.RoleExistsAsync(roleName); + if (!roleExist) + { + // Create the roles and seed them to the database + await roleManager.CreateAsync(new IdentityRole(roleName)); + } + } + } + + /// + /// Seeding user into the database. + /// + private static async Task SeedAdminUser(UserManager userManager) + { + IdentityUser? adminUser = await userManager.FindByEmailAsync("admin@example.com"); + if (adminUser == null) + { + adminUser = new IdentityUser + { + UserName = "admin@example.com", + Email = "admin@example.com", + }; + + IdentityResult? result = await userManager.CreateAsync(adminUser, "P@ssw0rd"); + if (result.Succeeded) + { + await userManager.AddToRoleAsync(adminUser, "Admin"); + } + } + } + } +} diff --git a/AdvancedAPI.Data/Migrations/20240719221124_InitialCreate.Designer.cs b/AdvancedAPI.Data/Migrations/20240719221124_InitialCreate.Designer.cs new file mode 100644 index 0000000..93cb12e --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240719221124_InitialCreate.Designer.cs @@ -0,0 +1,278 @@ +// +using System; +using AdvancedAPI.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + [DbContext(typeof(AdvancedApiContext))] + [Migration("20240719221124_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.32") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AdvancedAPI.Data/Migrations/20240719221124_InitialCreate.cs b/AdvancedAPI.Data/Migrations/20240719221124_InitialCreate.cs new file mode 100644 index 0000000..6a040ab --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240719221124_InitialCreate.cs @@ -0,0 +1,221 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AdvancedAPI.Data.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), + 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/AdvancedAPI.Data/Migrations/20240719221250_CreateIdentitySchema.Designer.cs b/AdvancedAPI.Data/Migrations/20240719221250_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..d13715a --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240719221250_CreateIdentitySchema.Designer.cs @@ -0,0 +1,278 @@ +// +using System; +using AdvancedAPI.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + [DbContext(typeof(AdvancedApiContext))] + [Migration("20240719221250_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.32") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AdvancedAPI.Data/Migrations/20240719221250_CreateIdentitySchema.cs b/AdvancedAPI.Data/Migrations/20240719221250_CreateIdentitySchema.cs new file mode 100644 index 0000000..fe4e94e --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240719221250_CreateIdentitySchema.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/AdvancedAPI.Data/Migrations/AdvancedApiContextModelSnapshot.cs b/AdvancedAPI.Data/Migrations/AdvancedApiContextModelSnapshot.cs new file mode 100644 index 0000000..d27c4fd --- /dev/null +++ b/AdvancedAPI.Data/Migrations/AdvancedApiContextModelSnapshot.cs @@ -0,0 +1,276 @@ +// +using System; +using AdvancedAPI.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + [DbContext(typeof(AdvancedApiContext))] + partial class AdvancedApiContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.32") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AdvancedAPI.Data/Program.cs b/AdvancedAPI.Data/Program.cs index 8490520..8ddb4d3 100644 --- a/AdvancedAPI.Data/Program.cs +++ b/AdvancedAPI.Data/Program.cs @@ -1,16 +1,9 @@ using AdvancedAPI.Data; -using Microsoft.EntityFrameworkCore; IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices( (context, services) => { - var connectionString = context.Configuration.GetConnectionString("DefaultConnection"); - - // Register ApplicationDbContext with the connection string - services.AddDbContext(options => - options.UseSqlServer(connectionString)); - services.AddHostedService(); }) .Build(); diff --git a/AdvancedAPI.Data/Repositories/IdentityRepository.cs b/AdvancedAPI.Data/Repositories/IdentityRepository.cs new file mode 100644 index 0000000..29a584a --- /dev/null +++ b/AdvancedAPI.Data/Repositories/IdentityRepository.cs @@ -0,0 +1,25 @@ +using AdvancedAPI.Data.Repositories.Interfaces; +using Microsoft.AspNetCore.Identity; + +namespace AdvancedAPI.Data.Repositories; + +/// +public class IdentityRepository : IIdentityRepository +{ + private readonly UserManager _userManager; + + /// + /// Constructor. + /// + public IdentityRepository(UserManager userManager) + { + _userManager = userManager; + } + + /// + public async Task GetUser(string userName) => await _userManager.FindByNameAsync(userName); + + /// + public async Task CheckPassword(IdentityUser user, string password) => + await _userManager.CheckPasswordAsync(user, password); +} diff --git a/AdvancedAPI.Data/Repositories/Interfaces/IIdentityRepository.cs b/AdvancedAPI.Data/Repositories/Interfaces/IIdentityRepository.cs new file mode 100644 index 0000000..57758ea --- /dev/null +++ b/AdvancedAPI.Data/Repositories/Interfaces/IIdentityRepository.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; + +namespace AdvancedAPI.Data.Repositories.Interfaces; + +/// +/// Identity repository. +/// +public interface IIdentityRepository +{ + /// + /// Getting the user from User manager. + /// + Task GetUser(string userName); + + /// + /// Checks the password of the user with user manager. + /// + Task CheckPassword(IdentityUser user, string password); +} diff --git a/AdvancedAPI.Data/ViewModels/Authentication/LoginRequestModel.cs b/AdvancedAPI.Data/ViewModels/Authentication/LoginRequestModel.cs new file mode 100644 index 0000000..375a8fa --- /dev/null +++ b/AdvancedAPI.Data/ViewModels/Authentication/LoginRequestModel.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace AdvancedAPI.Data.ViewModels.Authentication; + +/// +/// Request model to login. +/// +[JsonObject] +public class LoginRequestModel +{ + /// + /// user name. + /// + [JsonProperty("User name")] + public string Username { get; set; } + + /// + /// Password. + /// + [JsonProperty("Password")] + public string Password { get; set; } +} diff --git a/AdvancedAPI.Data/ViewModels/Houses/HouseResponseModel.cs b/AdvancedAPI.Data/ViewModels/Houses/HouseResponseModel.cs index 00d7421..0d3db78 100644 --- a/AdvancedAPI.Data/ViewModels/Houses/HouseResponseModel.cs +++ b/AdvancedAPI.Data/ViewModels/Houses/HouseResponseModel.cs @@ -11,6 +11,6 @@ public class HouseResponseModel /// /// Name of the street where the house is located. /// - [JsonProperty("street Name")] + [JsonProperty("street name")] public string StreetName { get; set; } = string.Empty; } diff --git a/AdvancedAPI.Data/appsettings.json b/AdvancedAPI.Data/appsettings.json index 8f9438f..3a6e1a6 100644 --- a/AdvancedAPI.Data/appsettings.json +++ b/AdvancedAPI.Data/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=server;Database=database;User Id=username;Password=password;" + "DefaultConnection": "Server=DESKTOP-HFMAPSN\\SQLEXPRESS;Database=master;Trusted_Connection=True;Integrated Security=True;" }, "Logging": { "LogLevel": { diff --git a/AdvancedAPI/BaseControllers/AdminBaseController.cs b/AdvancedAPI/BaseControllers/AdminBaseController.cs new file mode 100644 index 0000000..10c38a9 --- /dev/null +++ b/AdvancedAPI/BaseControllers/AdminBaseController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; + +namespace AdvancedAPI.BaseControllers; + +/// +/// Controller for admin related endpoints. +/// +[Microsoft.AspNetCore.Components.Route("admin")] +[ApiExplorerSettings(GroupName = "Admin")] +public class AdminBaseController : BaseController +{ + /// + /// Constructor. + /// + public AdminBaseController() + { + } +} diff --git a/AdvancedAPI/BaseControllers/BaseController.cs b/AdvancedAPI/BaseControllers/BaseController.cs new file mode 100644 index 0000000..0d1a026 --- /dev/null +++ b/AdvancedAPI/BaseControllers/BaseController.cs @@ -0,0 +1,58 @@ +using AdvancedAPI.Data.ViewModels; +using Microsoft.AspNetCore.Mvc; + +namespace AdvancedAPI.BaseControllers; + +/// +/// Base controller. +/// +[ApiController] +[Route("api")] +public class BaseController : ControllerBase +{ + /// + /// Constructor. + /// + public BaseController() + { + } + + /// + /// Returns an error when user is sending invalid request data. + /// + protected BadRequestObjectResult BadRequestResult(string? message) + { + return BadRequest( + new ErrorResponseModel + { + Code = 400, + Message = !string.IsNullOrEmpty(message) ? message : "The data you sent to the endpoint is invalid.", + }); + } + + /// + /// Returns an error when user is not authorized. + /// + protected UnauthorizedObjectResult UnauthorizedResult(string? message) + { + return Unauthorized( + new ErrorResponseModel + { + Code = 401, + Message = !string.IsNullOrEmpty(message) ? message : "You are not authorized for this request.", + }); + } + + /// + /// Returns an error when object is not found. + /// + protected NotFoundObjectResult NotFoundResult(string? message) + { + return NotFound( + new ErrorResponseModel + { + Code = 404, + Message = !string.IsNullOrEmpty(message) ? message : "Could not find any result", + }); + } +} diff --git a/AdvancedAPI/Controllers/AuthenticationContoller.cs b/AdvancedAPI/Controllers/AuthenticationContoller.cs new file mode 100644 index 0000000..b5a5f69 --- /dev/null +++ b/AdvancedAPI/Controllers/AuthenticationContoller.cs @@ -0,0 +1,47 @@ +using System.IdentityModel.Tokens.Jwt; +using AdvancedAPI.BaseControllers; +using AdvancedAPI.Data.ViewModels.Authentication; +using Business.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace AdvancedAPI.Controllers; + +/// +/// Authentication endpoints. +/// +[Microsoft.AspNetCore.Components.Route("authentication")] +public class AuthenticationContoller : BaseController +{ + private readonly IAuthenticationService _authenticationService; + private readonly IConfiguration _configuration; + + /// + /// Constructor. + /// + public AuthenticationContoller(IAuthenticationService authenticationService, IConfiguration configuration) + { + _authenticationService = authenticationService; + _configuration = configuration; + } + + /// + /// endpoint to log in. + /// + [HttpPost] + [Route("login")] + public async Task Login([FromBody] LoginRequestModel model) + { + JwtSecurityToken? token = await _authenticationService.Login(model); + + if (token != null) + { + return Ok(new + { + token = new JwtSecurityTokenHandler().WriteToken(token), + expiration = token.ValidTo, + }); + } + + return UnauthorizedResult(null); + } +} diff --git a/AdvancedAPI/Controllers/HouseController.cs b/AdvancedAPI/Controllers/HouseController.cs index 65c4c14..356c4c5 100644 --- a/AdvancedAPI/Controllers/HouseController.cs +++ b/AdvancedAPI/Controllers/HouseController.cs @@ -1,4 +1,5 @@ -using AdvancedAPI.Data.ViewModels; +using AdvancedAPI.BaseControllers; +using AdvancedAPI.Data.ViewModels; using AdvancedAPI.Data.ViewModels.Houses; using Business.Services.Interfaces; using Microsoft.AspNetCore.Mvc; @@ -8,9 +9,8 @@ namespace AdvancedAPI.Controllers; /// /// House api provided operation to main the houses. /// -[ApiController] -[Route("api/house")] -public class HouseController : ControllerBase +[Route("house")] +public class HouseController : BaseController { private readonly IHouseService _houseService; @@ -35,12 +35,7 @@ public async Task Overview(CancellationToken cancellationToken = if (houseResponseModels == null || !houseResponseModels.Any()) { - return NotFound( - new ErrorResponseModel - { - Code = 404, - Message = "No houses found", - }); + return NotFoundResult("Could not find any houses"); } return Ok(new List()); diff --git a/AdvancedAPI/Program.cs b/AdvancedAPI/Program.cs index 022cbea..da32119 100644 --- a/AdvancedAPI/Program.cs +++ b/AdvancedAPI/Program.cs @@ -1,25 +1,80 @@ using System.Reflection; +using System.Text; +using AdvancedAPI.Data; +using AdvancedAPI.Data.Repositories; +using AdvancedAPI.Data.Repositories.Interfaces; +using Business.Services; +using Business.Services.Interfaces; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); -builder.Services.AddSwaggerGen(c => -{ - c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo +// Services. +builder.Services.AddScoped(); + +// Repositories. +builder.Services.AddScoped(); + +builder.Services.AddSwaggerGen( + c => { - Title = "AdvancedAPI", - Version = "v1", - Description = "The Advanced API of DustSwiffer", + c.SwaggerDoc( + "v1", + new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "AdvancedAPI", + Version = "v1", + Description = "The Advanced API of DustSwiffer", + }); + + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); }); - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - c.IncludeXmlComments(xmlPath); -}); +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); + +// Register ApplicationDbContext with the connection string +builder.Services.AddDbContext( + options => + options.UseSqlServer(connectionString)); + +builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); -var app = builder.Build(); +builder.Services.AddScoped(); + +// Configure JWT authentication +// Get the JWT secret key from configuration +string jwtKey = builder.Configuration["Jwt:Key"]; +byte[] key = Encoding.ASCII.GetBytes(jwtKey); + +builder.Services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(x => + { + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + }; + }); + +WebApplication app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) @@ -35,17 +90,35 @@ app.UseRouting(); +app.UseAuthentication(); app.UseAuthorization(); app.UseSwagger(); -app.UseSwaggerUI(c => -{ - c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); - c.RoutePrefix = string.Empty; // Set Swagger UI at the app's root -}); +app.UseSwaggerUI( + c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + c.RoutePrefix = string.Empty; // Set Swagger UI at the app's root + }); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); +// Seed the database with initial data +using (IServiceScope scope = app.Services.CreateScope()) +{ + IServiceProvider services = scope.ServiceProvider; + + try + { + await DbInitializer.Initialize(services); + } + catch (Exception ex) + { + ILogger logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred seeding the DB."); + } +} + app.Run(); diff --git a/AdvancedAPI/appsettings.json b/AdvancedAPI/appsettings.json index 10f68b8..3d9ca42 100644 --- a/AdvancedAPI/appsettings.json +++ b/AdvancedAPI/appsettings.json @@ -1,4 +1,12 @@ { + "ConnectionStrings": { + "DefaultConnection": "Server=DESKTOP-HFMAPSN\\SQLEXPRESS;Database=master;Trusted_Connection=True;Integrated Security=True;" + }, + "Jwt": { + "Key": "YourVeryLongAndSecureKeyForJWTAuthenticationWhichShouldBeAtLeast32Bytes", + "Issuer": "yourdomain.com", + "Audience": "yourdomain.com" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/AdvancedAPI/stylecop.json b/AdvancedAPI/stylecop.json index b2f6c34..448e2ee 100644 --- a/AdvancedAPI/stylecop.json +++ b/AdvancedAPI/stylecop.json @@ -25,8 +25,7 @@ "systemUsingDirectivesFirst": true }, "namingRules": { - "allowCommonHungarianPrefixes": true, - + "allowCommonHungarianPrefixes": true }, "layoutRules": { "newlineAtEndOfFile": "require"