From 2bd34ec0229e1dba2e9cf3ef3ed4e9dfb79d70c0 Mon Sep 17 00:00:00 2001 From: Charles Showalter Date: Thu, 19 May 2022 13:50:10 -0700 Subject: [PATCH] Begin adding Identity --- .gitignore | 3 +- API/API.csproj | 2 +- API/Controllers/AccountController.cs | 54 +++ API/Dtos/LoginDto.cs | 13 + API/Dtos/RegisterDto.cs | 14 + API/Dtos/UserDto.cs | 14 + API/Extensions/IdentityServiceExtensions.cs | 21 ++ API/Program.cs | 9 + API/Startup.cs | 3 + API/appsettings.Development.json | 1 + Core/Core.csproj | 4 + Core/Entities/Identity/Address.cs | 20 ++ Core/Entities/Identity/AppUser.cs | 10 + Core/Interfaces/ITokenService.cs | 13 + .../Identity/AppIdentityDbContext.cs | 18 + .../Identity/AppIdentityDbContextSeed.cs | 30 ++ ...20220519173706_IdentityInitial.Designer.cs | 322 ++++++++++++++++++ .../20220519173706_IdentityInitial.cs | 254 ++++++++++++++ .../AppIdentityDbContextModelSnapshot.cs | 320 +++++++++++++++++ Infrastructure/Infrastructure.csproj | 3 + Infrastructure/Services/TokenService.cs | 43 +++ client/src/app/app-routing.module.ts | 1 + .../app/checkout/checkout-routing.module.ts | 18 + .../src/app/checkout/checkout.component.html | 3 + .../src/app/checkout/checkout.component.scss | 0 client/src/app/checkout/checkout.component.ts | 15 + client/src/app/checkout/checkout.module.ts | 17 + client/src/app/checkout/checkout.service.ts | 9 + 28 files changed, 1232 insertions(+), 2 deletions(-) create mode 100644 API/Controllers/AccountController.cs create mode 100644 API/Dtos/LoginDto.cs create mode 100644 API/Dtos/RegisterDto.cs create mode 100644 API/Dtos/UserDto.cs create mode 100644 API/Extensions/IdentityServiceExtensions.cs create mode 100644 Core/Entities/Identity/Address.cs create mode 100644 Core/Entities/Identity/AppUser.cs create mode 100644 Core/Interfaces/ITokenService.cs create mode 100644 Infrastructure/Identity/AppIdentityDbContext.cs create mode 100644 Infrastructure/Identity/AppIdentityDbContextSeed.cs create mode 100644 Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.Designer.cs create mode 100644 Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.cs create mode 100644 Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs create mode 100644 Infrastructure/Services/TokenService.cs create mode 100644 client/src/app/checkout/checkout-routing.module.ts create mode 100644 client/src/app/checkout/checkout.component.html create mode 100644 client/src/app/checkout/checkout.component.scss create mode 100644 client/src/app/checkout/checkout.component.ts create mode 100644 client/src/app/checkout/checkout.module.ts create mode 100644 client/src/app/checkout/checkout.service.ts diff --git a/.gitignore b/.gitignore index 6950d24..f70c650 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ obj bin appsettings.json -*.db* \ No newline at end of file +*.db* +*.rdb* \ No newline at end of file diff --git a/API/API.csproj b/API/API.csproj index 71218b9..662ba75 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs new file mode 100644 index 0000000..fe2e7dd --- /dev/null +++ b/API/Controllers/AccountController.cs @@ -0,0 +1,54 @@ +using API.Dtos; +using API.Errors; +using Core.Entities.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers +{ + public class AccountController : BaseApiController + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + public AccountController(UserManager userManager, SignInManager signInManager) + { + _signInManager = signInManager; + _userManager = userManager; + } + + [HttpPost("login")] + public async Task> Login(LoginDto loginDto) + { + var user = await _userManager.FindByEmailAsync(loginDto.Email); + if (user == null) return Unauthorized(new ApiResponse(401)); + var results = await _signInManager.CheckPasswordSignInAsync(user, loginDto.Password, false); + if(!results.Succeeded) return Unauthorized(new ApiResponse(401)); + return new UserDto + { + Email = user.Email, + Token = "This will be a token", + DisplayName = user.DisplayName + }; + } + + [HttpPost("register")] + public async Task> Register(RegisterDto registerDto) + { + var user = new AppUser + { + DisplayName = registerDto.DisplayName, + Email = registerDto.Email, + UserName = registerDto.Email + }; + + var results = await _userManager.CreateAsync(user, registerDto.Password); + if(!results.Succeeded) return BadRequest(new ApiResponse(400)); + return new UserDto + { + DisplayName = user.DisplayName, + Token = "This will be a token", + Email = user.Email + }; + } + } +} \ No newline at end of file diff --git a/API/Dtos/LoginDto.cs b/API/Dtos/LoginDto.cs new file mode 100644 index 0000000..0f0cdef --- /dev/null +++ b/API/Dtos/LoginDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace API.Dtos +{ + public class LoginDto + { + public string Email { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/API/Dtos/RegisterDto.cs b/API/Dtos/RegisterDto.cs new file mode 100644 index 0000000..43684ff --- /dev/null +++ b/API/Dtos/RegisterDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace API.Dtos +{ + public class RegisterDto + { + public string DisplayName { get; set; } + public string Email { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/API/Dtos/UserDto.cs b/API/Dtos/UserDto.cs new file mode 100644 index 0000000..afaf6c8 --- /dev/null +++ b/API/Dtos/UserDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace API.Dtos +{ + public class UserDto + { + public string Email { get; set; } + public string DisplayName { get; set; } + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs new file mode 100644 index 0000000..c43a1a8 --- /dev/null +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -0,0 +1,21 @@ +using Core.Entities.Identity; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; + +namespace API.Extensions +{ + public static class IdentityServiceExtensions + { + public static IServiceCollection AddIdentityServices(this IServiceCollection services) + { + var builder = services.AddIdentityCore(); + builder = new IdentityBuilder(builder.UserType, builder.Services); + builder.AddEntityFrameworkStores(); + builder.AddSignInManager>(); + + services.AddAuthentication(); + + return services; + } + } +} \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index 11a5d97..6250104 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,5 +1,8 @@ +using Core.Entities.Identity; using Infrastructure; using Infrastructure.Data; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API @@ -18,6 +21,12 @@ namespace API var context = services.GetRequiredService(); await context.Database.MigrateAsync(); await StoreContextSeed.SeedAsync(context, loggerFactory); + + var userManager = services.GetRequiredService>(); + var identityContext = services.GetRequiredService(); + await identityContext.Database.MigrateAsync(); + await AppIdentityDbContextSeed.SeedUsersAsync(userManager); + } catch (Exception ex) { diff --git a/API/Startup.cs b/API/Startup.cs index 85a6fb1..f926793 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -2,6 +2,7 @@ using API.Extensions; using API.Helpers; using API.Middleware; using Infrastructure.Data; +using Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; @@ -22,8 +23,10 @@ namespace API services.AddControllers(); services.AddApplicationServices(); + services.AddIdentityServices(); services.AddSwaggerDocumentation(); services.AddDbContext(x => x.UseSqlite(_config.GetConnectionString("DefaultConnection"))); + services.AddDbContext(x => x.UseSqlite(_config.GetConnectionString("IdentityConnection"))); services.AddSingleton(c => { var configuration = ConfigurationOptions.Parse(_config.GetConnectionString("redis"), true); return ConnectionMultiplexer.Connect(configuration); diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index 1029181..972b855 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -7,6 +7,7 @@ }, "ConnectionStrings": { "DefaultConnection": "Data source=ecommerce.db", + "IdentityConnection": "Data source=indentity.db", "Redis": "localhost" }, "ApiUrl": "https://localhost:5001/" diff --git a/Core/Core.csproj b/Core/Core.csproj index d215c71..1ae26dc 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -5,4 +5,8 @@ enable + + + + diff --git a/Core/Entities/Identity/Address.cs b/Core/Entities/Identity/Address.cs new file mode 100644 index 0000000..bcf20bf --- /dev/null +++ b/Core/Entities/Identity/Address.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Core.Entities.Identity +{ + public class Address + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string State { get; set; } + public string ZipCode { get; set; } + + [Required] + public string AppUserId { get; set; } + public AppUser AppUser { get; set; } + + } +} \ No newline at end of file diff --git a/Core/Entities/Identity/AppUser.cs b/Core/Entities/Identity/AppUser.cs new file mode 100644 index 0000000..da3e1f1 --- /dev/null +++ b/Core/Entities/Identity/AppUser.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; + +namespace Core.Entities.Identity +{ + public class AppUser : IdentityUser + { + public string DisplayName { get; set; } + public Address Address { get; set; } + } +} \ No newline at end of file diff --git a/Core/Interfaces/ITokenService.cs b/Core/Interfaces/ITokenService.cs new file mode 100644 index 0000000..6e4acfa --- /dev/null +++ b/Core/Interfaces/ITokenService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Core.Entities.Identity; + +namespace Core.Interfaces +{ + public interface ITokenService + { + string CreateToken(AppUser user); + } +} \ No newline at end of file diff --git a/Infrastructure/Identity/AppIdentityDbContext.cs b/Infrastructure/Identity/AppIdentityDbContext.cs new file mode 100644 index 0000000..3bceed1 --- /dev/null +++ b/Infrastructure/Identity/AppIdentityDbContext.cs @@ -0,0 +1,18 @@ +using Core.Entities.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Identity +{ + public class AppIdentityDbContext : IdentityDbContext + { + public AppIdentityDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Identity/AppIdentityDbContextSeed.cs b/Infrastructure/Identity/AppIdentityDbContextSeed.cs new file mode 100644 index 0000000..7440420 --- /dev/null +++ b/Infrastructure/Identity/AppIdentityDbContextSeed.cs @@ -0,0 +1,30 @@ +using Core.Entities.Identity; +using Microsoft.AspNetCore.Identity; + +namespace Infrastructure.Identity +{ + public class AppIdentityDbContextSeed + { + public static async Task SeedUsersAsync(UserManager userManager) + { + if(!userManager.Users.Any()){ + var user = new AppUser + { + DisplayName = "Bob", + Email = "bob@test.com", + UserName = "bob@test.com", + Address = new Address { + FirstName = "Bob", + LastName = "Bobbity", + Street = "10 The Street", + City = "New York", + State = "NY", + ZipCode = "90210" + } + }; + + await userManager.CreateAsync(user, "Pa$$w0rd"); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.Designer.cs b/Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.Designer.cs new file mode 100644 index 0000000..c9db779 --- /dev/null +++ b/Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.Designer.cs @@ -0,0 +1,322 @@ +// +using System; +using Infrastructure.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Identity.Migrations +{ + [DbContext(typeof(AppIdentityDbContext))] + [Migration("20220519173706_IdentityInitial")] + partial class IdentityInitial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + modelBuilder.Entity("Core.Entities.Identity.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.Property("Street") + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("Address"); + }); + + modelBuilder.Entity("Core.Entities.Identity.AppUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Core.Entities.Identity.Address", b => + { + b.HasOne("Core.Entities.Identity.AppUser", "AppUser") + .WithOne("Address") + .HasForeignKey("Core.Entities.Identity.Address", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + 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("Core.Entities.Identity.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Entities.Identity.AppUser", 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("Core.Entities.Identity.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Entities.Identity.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Core.Entities.Identity.AppUser", b => + { + b.Navigation("Address"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.cs b/Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.cs new file mode 100644 index 0000000..ffd2805 --- /dev/null +++ b/Infrastructure/Identity/Migrations/20220519173706_IdentityInitial.cs @@ -0,0 +1,254 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Identity.Migrations +{ + public partial class IdentityInitial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: true), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", 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: "Address", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FirstName = table.Column(type: "TEXT", nullable: true), + LastName = table.Column(type: "TEXT", nullable: true), + Street = table.Column(type: "TEXT", nullable: true), + City = table.Column(type: "TEXT", nullable: true), + State = table.Column(type: "TEXT", nullable: true), + ZipCode = table.Column(type: "TEXT", nullable: true), + AppUserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Address", x => x.Id); + table.ForeignKey( + name: "FK_Address_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", 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: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", 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: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", 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: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", 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_Address_AppUserId", + table: "Address", + column: "AppUserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + 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); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Address"); + + 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/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs b/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs new file mode 100644 index 0000000..20c7f3c --- /dev/null +++ b/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs @@ -0,0 +1,320 @@ +// +using System; +using Infrastructure.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Identity.Migrations +{ + [DbContext(typeof(AppIdentityDbContext))] + partial class AppIdentityDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + modelBuilder.Entity("Core.Entities.Identity.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.Property("Street") + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("Address"); + }); + + modelBuilder.Entity("Core.Entities.Identity.AppUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Core.Entities.Identity.Address", b => + { + b.HasOne("Core.Entities.Identity.AppUser", "AppUser") + .WithOne("Address") + .HasForeignKey("Core.Entities.Identity.Address", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + 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("Core.Entities.Identity.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Entities.Identity.AppUser", 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("Core.Entities.Identity.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Entities.Identity.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Core.Entities.Identity.AppUser", b => + { + b.Navigation("Address"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index f5a3568..6828b74 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -5,8 +5,11 @@ + + + diff --git a/Infrastructure/Services/TokenService.cs b/Infrastructure/Services/TokenService.cs new file mode 100644 index 0000000..2c1a2c1 --- /dev/null +++ b/Infrastructure/Services/TokenService.cs @@ -0,0 +1,43 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Core.Entities.Identity; +using Core.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace Infrastructure.Services +{ + public class TokenService : ITokenService + { + private readonly IConfiguration _config; + private readonly SymmetricSecurityKey _key; + public TokenService(IConfiguration config) + { + _config = config; + _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Token:Key"])); + } + + public string CreateToken(AppUser user) + { + var claims = new List + { + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.GivenName, user.DisplayName) + }; + + var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.Now.AddDays(7), + SigningCredentials = creds, + Issuer = _config["Token:Issuer"] + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + } +} \ No newline at end of file diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index a6d5bc3..12264f8 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -12,6 +12,7 @@ const routes: Routes = [ {path: 'not-found', component: NotFoundComponent, data: {breadcrumb: 'Not Found'}}, {path: 'shop', loadChildren: ()=> import('./shop/shop.module').then(mod => mod.ShopModule), data: {breadcrumb: 'Shop'}}, {path: 'basket', loadChildren: ()=> import('./basket/basket.module').then(mod => mod.BasketModule), data: {breadcrumb: 'Shopping Cart'}}, + {path: 'checkout', loadChildren: ()=> import('./checkout/checkout.module').then(mod => mod.CheckoutModule), data: {breadcrumb: 'Checkout'}}, {path: '**', redirectTo: 'not-found', pathMatch: 'full'} ]; diff --git a/client/src/app/checkout/checkout-routing.module.ts b/client/src/app/checkout/checkout-routing.module.ts new file mode 100644 index 0000000..54b0bb9 --- /dev/null +++ b/client/src/app/checkout/checkout-routing.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CheckoutComponent } from './checkout.component'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + {path: '', component: CheckoutComponent} +] + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + RouterModule.forChild(routes) + ], + exports: [RouterModule] +}) +export class CheckoutRoutingModule { } diff --git a/client/src/app/checkout/checkout.component.html b/client/src/app/checkout/checkout.component.html new file mode 100644 index 0000000..abf724e --- /dev/null +++ b/client/src/app/checkout/checkout.component.html @@ -0,0 +1,3 @@ +
+

Only authorized users should be able to see this.

+
diff --git a/client/src/app/checkout/checkout.component.scss b/client/src/app/checkout/checkout.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/checkout/checkout.component.ts b/client/src/app/checkout/checkout.component.ts new file mode 100644 index 0000000..b0398d6 --- /dev/null +++ b/client/src/app/checkout/checkout.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-checkout', + templateUrl: './checkout.component.html', + styleUrls: ['./checkout.component.scss'] +}) +export class CheckoutComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/client/src/app/checkout/checkout.module.ts b/client/src/app/checkout/checkout.module.ts new file mode 100644 index 0000000..cacddad --- /dev/null +++ b/client/src/app/checkout/checkout.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CheckoutComponent } from './checkout.component'; +import { CheckoutRoutingModule } from './checkout-routing.module'; + + + +@NgModule({ + declarations: [ + CheckoutComponent + ], + imports: [ + CommonModule, + CheckoutRoutingModule + ] +}) +export class CheckoutModule { } diff --git a/client/src/app/checkout/checkout.service.ts b/client/src/app/checkout/checkout.service.ts new file mode 100644 index 0000000..d9285b6 --- /dev/null +++ b/client/src/app/checkout/checkout.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CheckoutService { + + constructor() { } +}