Adding initial login code

This commit is contained in:
Chalez
2024-02-12 00:45:16 -05:00
parent 3eb3d681e0
commit d63c54b3ee
43 changed files with 1996 additions and 0 deletions
@@ -0,0 +1,12 @@
namespace mediasrv.ApplicationCore.Constants;
public class AuthorizationConstants
{
public const string AUTH_KEY = "AuthKeyOfDoomThatMustBeAMinimumNumberOfBytes";
// TODO: Don't use this in production
public const string DEFAULT_PASSWORD = "Pass@word1";
// TODO: Change this to an environment variable
public const string JWT_SECRET_KEY = "SecretKeyOfDoomThatMustBeAMinimumNumberOfBytes";
}
@@ -0,0 +1,86 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Threading.Tasks;
using BlazorShared.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
namespace BlazorAdmin;
public class CustomAuthStateProvider : AuthenticationStateProvider
{
// TODO: Get Default Cache Duration from Config
private static readonly TimeSpan UserCacheRefreshInterval = TimeSpan.FromSeconds(60);
private readonly HttpClient _httpClient;
private readonly ILogger<CustomAuthStateProvider> _logger;
private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0);
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
public CustomAuthStateProvider(HttpClient httpClient,
ILogger<CustomAuthStateProvider> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
return new AuthenticationState(await GetUser(useCache: true));
}
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
{
var now = DateTimeOffset.Now;
if (useCache && now < _userLastCheck + UserCacheRefreshInterval)
{
return _cachedUser;
}
_cachedUser = await FetchUser();
_userLastCheck = now;
return _cachedUser;
}
private async Task<ClaimsPrincipal> FetchUser()
{
UserInfo user = null;
try
{
_logger.LogInformation("Fetching user details from web api.");
user = await _httpClient.GetFromJsonAsync<UserInfo>("User");
}
catch (Exception exc)
{
_logger.LogWarning(exc, "Fetching user failed.");
}
if (user == null || !user.IsAuthenticated)
{
return new ClaimsPrincipal(new ClaimsIdentity());
}
var identity = new ClaimsIdentity(
nameof(CustomAuthStateProvider),
user.NameClaimType,
user.RoleClaimType);
if (user.Claims != null)
{
foreach (var claim in user.Claims)
{
identity.AddClaim(new Claim(claim.Type, claim.Value));
}
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.Token);
return new ClaimsPrincipal(identity);
}
}
@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Components;
namespace BlazorAdmin.Helpers;
public class BlazorComponent : ComponentBase
{
private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance;
protected override void OnInitialized()
{
_refresh.RefreshRequested += DoRefresh;
base.OnInitialized();
}
public void CallRequestRefresh()
{
_refresh.CallRequestRefresh();
}
private void DoRefresh()
{
StateHasChanged();
}
}
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Components;
namespace BlazorAdmin.Helpers;
public class BlazorLayoutComponent : LayoutComponentBase
{
private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance;
protected override void OnInitialized()
{
_refresh.RefreshRequested += DoRefresh;
base.OnInitialized();
}
public void CallRequestRefresh()
{
_refresh.CallRequestRefresh();
}
private void DoRefresh()
{
StateHasChanged();
}
}
@@ -0,0 +1,23 @@
using System;
namespace BlazorAdmin.Helpers;
internal sealed class RefreshBroadcast
{
private static readonly Lazy<RefreshBroadcast>
Lazy =
new Lazy<RefreshBroadcast>
(() => new RefreshBroadcast());
public static RefreshBroadcast Instance => Lazy.Value;
private RefreshBroadcast()
{
}
public event Action RefreshRequested;
public void CallRequestRefresh()
{
RefreshRequested?.Invoke();
}
}
@@ -0,0 +1,87 @@
using System;
using BlazorAdmin.Services;
using Microsoft.AspNetCore.Components;
namespace BlazorAdmin.Helpers;
public class ToastComponent : ComponentBase, IDisposable
{
[Inject]
ToastService ToastService
{
get;
set;
}
protected string Heading
{
get;
set;
}
protected string Message
{
get;
set;
}
protected bool IsVisible
{
get;
set;
}
protected string BackgroundCssClass
{
get;
set;
}
protected string IconCssClass
{
get;
set;
}
protected override void OnInitialized()
{
ToastService.OnShow += ShowToast;
ToastService.OnHide += HideToast;
}
private void ShowToast(string message, ToastLevel level)
{
BuildToastSettings(level, message);
IsVisible = true;
StateHasChanged();
}
private void HideToast()
{
IsVisible = false;
StateHasChanged();
}
private void BuildToastSettings(ToastLevel level, string message)
{
switch (level)
{
case ToastLevel.Info:
BackgroundCssClass = "bg-info";
IconCssClass = "info";
Heading = "Info";
break;
case ToastLevel.Success:
BackgroundCssClass = "bg-success";
IconCssClass = "check";
Heading = "Success";
break;
case ToastLevel.Warning:
BackgroundCssClass = "bg-warning";
IconCssClass = "exclamation";
Heading = "Warning";
break;
case ToastLevel.Error:
BackgroundCssClass = "bg-danger";
IconCssClass = "times";
Heading = "Error";
break;
}
Message = message;
}
public void Dispose()
{
ToastService.OnShow -= ShowToast;
}
}
@@ -0,0 +1,14 @@
@page "/logout"
@inject IJSRuntime JSRuntime
@inject HttpClient HttpClient
@inherits BlazorAdmin.Helpers.BlazorComponent
@code {
protected override async Task OnInitializedAsync()
{
await HttpClient.PostAsync("User/Logout", null);
await new Route(JSRuntime).RouteOutside("/Identity/Account/Login");
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace BlazorShared.Authorization;
public class ClaimValue
{
public ClaimValue()
{
}
public ClaimValue(string type, string value)
{
Type = type;
Value = value;
}
public string Type { get; set; }
public string Value { get; set; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace BlazorShared.Authorization;
public static class Constants
{
public static class Roles
{
public const string ADMINISTRATORS = "Administrators";
}
}
+13
View File
@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace BlazorShared.Authorization;
public class UserInfo
{
public static readonly UserInfo Anonymous = new UserInfo();
public bool IsAuthenticated { get; set; }
public string NameClaimType { get; set; }
public string RoleClaimType { get; set; }
public string Token { get; set; }
public IEnumerable<ClaimValue> Claims { get; set; }
}
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace mediasrv.Infrastructure.Identity;
public class AppIdentityDbContext : IdentityDbContext<ApplicationUser>
{
public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// 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);
}
}
@@ -0,0 +1,32 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using mediasrv.ApplicationCore.Constants;
namespace mediasrv.Infrastructure.Identity;
public class AppIdentityDbContextSeed
{
public static async Task SeedAsync(AppIdentityDbContext identityDbContext, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
if (identityDbContext.Database.IsSqlServer())
{
identityDbContext.Database.Migrate();
}
await roleManager.CreateAsync(new IdentityRole(BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS));
var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" };
await userManager.CreateAsync(defaultUser, AuthorizationConstants.DEFAULT_PASSWORD);
string adminUserName = "admin@microsoft.com";
var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName };
await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD);
adminUser = await userManager.FindByNameAsync(adminUserName);
if (adminUser != null)
{
await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS);
}
}
}
@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Identity;
namespace mediasrv.Infrastructure.Identity;
public class ApplicationUser : IdentityUser
{
}
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using mediasrv.ApplicationCore.Constants;
using mediasrv.ApplicationCore.Interfaces;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
namespace Microsoft.eShopWeb.Infrastructure.Identity;
public class IdentityTokenClaimService : ITokenClaimsService
{
private readonly UserManager<ApplicationUser> _userManager;
public IdentityTokenClaimService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<string> GetTokenAsync(string userName)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
var user = await _userManager.FindByNameAsync(userName);
if (user == null) throw new UserNotFoundException(userName);
var roles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim> { new Claim(ClaimTypes.Name, userName) };
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims.ToArray()),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
@@ -0,0 +1,273 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.eShopWeb.Infrastructure.Identity;
namespace Microsoft.eShopWeb.Infrastructure.Identity.Migrations
{
[DbContext(typeof(AppIdentityDbContext))]
[Migration("20201202111612_InitialIdentityModel")]
partial class InitialIdentityModel
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.UseIdentityColumns()
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("ProductVersion", "5.0.0");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.UseIdentityColumn();
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.UseIdentityColumn();
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("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");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,218 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Microsoft.eShopWeb.Infrastructure.Identity.Migrations;
public partial class InitialIdentityModel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false),
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
ClaimValue = table.Column<string>(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<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
ClaimValue = table.Column<string>(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<string>(type: "nvarchar(450)", nullable: false),
ProviderKey = table.Column<string>(type: "nvarchar(450)", nullable: false),
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
UserId = table.Column<string>(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<string>(type: "nvarchar(450)", nullable: false),
RoleId = table.Column<string>(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<string>(type: "nvarchar(450)", nullable: false),
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
Name = table.Column<string>(type: "nvarchar(450)", nullable: false),
Value = table.Column<string>(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");
}
}
@@ -0,0 +1,271 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.eShopWeb.Infrastructure.Identity;
namespace Microsoft.eShopWeb.Infrastructure.Identity.Migrations
{
[DbContext(typeof(AppIdentityDbContext))]
partial class AppIdentityDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.UseIdentityColumns()
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("ProductVersion", "5.0.0");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.UseIdentityColumn();
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.UseIdentityColumn();
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("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");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,10 @@
using System;
namespace mediasrv.Infrastructure.Identity;
public class UserNotFoundException : Exception
{
public UserNotFoundException(string userName) : base($"No user found with username: {userName}")
{
}
}
@@ -0,0 +1,7 @@
namespace mediasrv.PublicApi.AuthEndpoints;
public class AuthenticateRequest : BaseRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
@@ -0,0 +1,20 @@
using System;
namespace mediasrv.PublicApi.AuthEndpoints;
public class AuthenticateResponse : BaseResponse
{
public AuthenticateResponse(Guid correlationId) : base(correlationId)
{
}
public AuthenticateResponse()
{
}
public bool Result { get; set; } = false;
public string Token { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public bool IsLockedOut { get; set; } = false;
public bool IsNotAllowed { get; set; } = false;
public bool RequiresTwoFactor { get; set; } = false;
}
@@ -0,0 +1,17 @@
namespace mediasrv.PublicApi.AuthEndpoints;
public class ClaimValue
{
public ClaimValue()
{
}
public ClaimValue(string type, string value)
{
Type = type;
Value = value;
}
public string Type { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace mediasrv.PublicApi.AuthEndpoints;
public class UserInfo
{
public static readonly UserInfo Anonymous = new UserInfo();
public bool IsAuthenticated { get; set; }
public string NameClaimType { get; set; } = string.Empty;
public string RoleClaimType { get; set; } = string.Empty;
public IEnumerable<ClaimValue> Claims { get; set; } = new List<ClaimValue>();
}
@@ -0,0 +1,59 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Swashbuckle.AspNetCore.Annotations;
namespace mediasrv.PublicApi.AuthEndpoints;
/// <summary>
/// Authenticates a user
/// </summary>
public class AuthenticateEndpoint : EndpointBaseAsync
.WithRequest<AuthenticateRequest>
.WithActionResult<AuthenticateResponse>
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ITokenClaimsService _tokenClaimsService;
public AuthenticateEndpoint(SignInManager<ApplicationUser> signInManager,
ITokenClaimsService tokenClaimsService)
{
_signInManager = signInManager;
_tokenClaimsService = tokenClaimsService;
}
[HttpPost("api/authenticate")]
[SwaggerOperation(
Summary = "Authenticates a user",
Description = "Authenticates a user",
OperationId = "auth.authenticate",
Tags = new[] { "AuthEndpoints" })
]
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request,
CancellationToken cancellationToken = default)
{
var response = new AuthenticateResponse(request.CorrelationId());
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await _signInManager.PasswordSignInAsync(request.Username, request.Password, false, true);
response.Result = result.Succeeded;
response.IsLockedOut = result.IsLockedOut;
response.IsNotAllowed = result.IsNotAllowed;
response.RequiresTwoFactor = result.RequiresTwoFactor;
response.Username = request.Username;
if (result.Succeeded)
{
response.Token = await _tokenClaimsService.GetTokenAsync(request.Username);
}
return response;
}
}
@@ -0,0 +1,54 @@
using System;
using System.Net;
using System.Threading.Tasks;
using BlazorShared.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopWeb.ApplicationCore.Exceptions;
namespace mediasrv.PublicApi.Middleware;
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
public ExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
await HandleExceptionAsync(httpContext, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
if (exception is DuplicateException duplicationException)
{
context.Response.StatusCode = (int)HttpStatusCode.Conflict;
await context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = duplicationException.Message
}.ToString());
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = exception.Message
}.ToString());
}
}
}
+37
View File
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorAdmin.Shared;
/// <summary>
/// This is needed until 5.0 ships with native support
/// https://www.pragimtech.com/blog/blazor/inputselect-does-not-support-system.int32/
/// </summary>
/// <typeparam name="TValue"></typeparam>
public class CustomInputSelect<TValue> : InputSelect<TValue>
{
protected override bool TryParseValueFromString(string value, out TValue result,
out string validationErrorMessage)
{
if (typeof(TValue) == typeof(int))
{
if (int.TryParse(value, out var resultInt))
{
result = (TValue)(object)resultInt;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage =
$"The selected value {value} is not a valid number.";
return false;
}
}
else
{
return base.TryParseValueFromString(value, out result,
out validationErrorMessage);
}
}
}
+41
View File
@@ -0,0 +1,41 @@
@inject AuthenticationStateProvider AuthStateProvider
@inject IJSRuntime JSRuntime
@inherits BlazorAdmin.Helpers.BlazorLayoutComponent
<AuthorizeView Roles=@BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS>
<div class="sidebar">
<NavMenu />
</div>
</AuthorizeView>
<div class="main">
<div class="top-row px-4">
<a href="https://github.com/dotnet-architecture/eShopOnWeb" target="_blank" class="ml-md-auto">About eShopOnWeb</a>
</div>
<div class="content px-4">
<Toast></Toast>
@Body
</div>
</div>
@code
{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
if (authState.User == null)
{
await new Route(JSRuntime).RouteOutside("/Identity/Account/Login");
}
CallRequestRefresh();
}
await base.OnAfterRenderAsync(firstRender);
}
}
+44
View File
@@ -0,0 +1,44 @@
@inherits BlazorAdmin.Helpers.BlazorComponent
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">eShopOnWeb Admin</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="admin" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<AuthorizeView>
<Authorized>
<li class="nav-item px-3">
<NavLink class="nav-link" href="manage/my-account" Match="NavLinkMatch.All">
<span class="oi oi-person" aria-hidden="true"></span> @context.User.Identity.Name
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="logout">
<span class="oi oi-account-logout" aria-hidden="true"></span> Logout
</NavLink>
</li>
</Authorized>
</AuthorizeView>
</ul>
</div>
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
+12
View File
@@ -0,0 +1,12 @@
@using System.Web;
@inject NavigationManager Navigation
@inject IJSRuntime JsRuntime
@code {
protected override void OnInitialized()
{
var returnUrl = HttpUtility.UrlEncode($"/{Uri.EscapeDataString(Navigation.ToBaseRelativePath(Navigation.Uri))}");
JsRuntime.InvokeVoidAsync("location.replace", $"Identity/Account/Login?returnUrl={returnUrl}");
}
}
+9
View File
@@ -0,0 +1,9 @@
@inherits BlazorAdmin.Helpers.BlazorComponent
@namespace BlazorAdmin.Shared
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
+13
View File
@@ -0,0 +1,13 @@
@inherits BlazorAdmin.Helpers.ToastComponent
@namespace BlazorAdmin.Shared
<div class="toast @(IsVisible ? "toast-visible" : null) @BackgroundCssClass">
<div class="toast-icon">
<i class="fa fa-@IconCssClass" aria-hidden="true"></i>
</div>
<div class="toast-body">
<h5>@Heading</h5>
<p>@Message</p>
</div>
</div>
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Hosting;
[assembly: HostingStartup(typeof(Microsoft.eShopWeb.Web.Areas.Identity.IdentityHostingStartup))]
namespace Microsoft.eShopWeb.Web.Areas.Identity;
public class IdentityHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) =>
{
});
}
}
@@ -0,0 +1,12 @@
@page
@model ConfirmEmailModel
@{
ViewData["Title"] = "Confirm email";
}
<h2>@ViewData["Title"]</h2>
<div>
<p>
Thank you for confirming your email.
</p>
</div>
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using mediasrv.Infrastructure.Identity;
namespace mediasrv.Web.Areas.Identity.Pages.Account;
[AllowAnonymous]
public class ConfirmEmailModel : PageModel
{
private readonly UserManager<ApplicationUser> _userManager;
public ConfirmEmailModel(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<IActionResult> OnGetAsync(string userId, string code)
{
if (userId == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
}
return Page();
}
}
@@ -0,0 +1,63 @@
@page
@model LoginModel
@{
ViewData["Title"] = "Log in";
}
<div class="container account-login-container">
<h2>@ViewData["Title"]</h2>
<div class="row">
<div class="col-md-12">
<section>
<form method="post">
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-group">
<div class="checkbox">
<label asp-for="Input.RememberMe">
<input asp-for="Input.RememberMe" />
@Html.DisplayNameFor(m => m.Input.RememberMe)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
</div>
<div class="form-group">
<p>
<a asp-page="./ForgotPassword">Forgot your password?</a>
</p>
<p>
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
</p>
</div>
<p>
Note that for demo purposes you don't need to register and can login with these credentials:
</p>
<p>
User: <b>demouser@microsoft.com</b> OR <b>admin@microsoft.com</b>
</p>
<p>
Password: <b>Pass@word1</b>
</p>
</form>
</section>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,119 @@
using System.ComponentModel.DataAnnotations;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using mediasrv.ApplicationCore.Interfaces;
using mediasrv.Infrastructure.Identity;
namespace media.Web.Areas.Identity.Pages.Account;
[AllowAnonymous]
public class LoginModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
private readonly IBasketService _basketService;
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IBasketService basketService)
{
_signInManager = signInManager;
_logger = logger;
_basketService = basketService;
}
[BindProperty]
public required InputModel Input { get; set; }
public IList<AuthenticationScheme>? ExternalLogins { get; set; }
public string? ReturnUrl { get; set; }
[TempData]
public string? ErrorMessage { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
public string? Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string? Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
public async Task OnGetAsync(string? returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
returnUrl = returnUrl ?? Url.Content("~/");
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await _signInManager.PasswordSignInAsync(Input!.Email!, Input!.Password!,
false, true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
await TransferAnonymousBasketToUserAsync(Input?.Email);
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input?.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
private async Task TransferAnonymousBasketToUserAsync(string? userName)
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
var anonymousId = Request.Cookies[Constants.BASKET_COOKIENAME];
if (Guid.TryParse(anonymousId, out var _))
{
Guard.Against.NullOrEmpty(userName, nameof(userName));
await _basketService.TransferBasketAsync(anonymousId, userName);
}
Response.Cookies.Delete(Constants.BASKET_COOKIENAME);
}
}
}
@@ -0,0 +1,10 @@
@page
@model LogoutModel
@{
ViewData["Title"] = "Log out";
}
<header>
<h1>@ViewData["Title"]</h1>
<p>You have successfully logged out of the application.</p>
</header>
@@ -0,0 +1,52 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Configuration;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account;
//TODO : replace IMemoryCache by distributed cache if you are in multi-host scenario
public class LogoutModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LogoutModel> _logger;
private readonly IMemoryCache _cache;
public LogoutModel(SignInManager<ApplicationUser> signInManager, ILogger<LogoutModel> logger, IMemoryCache cache)
{
_signInManager = signInManager;
_logger = logger;
_cache = cache;
}
public void OnGet()
{
}
public async Task<IActionResult> OnPost(string? returnUrl = null)
{
await _signInManager.SignOutAsync();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var userId = _signInManager.Context.User.Claims.First(c => c.Type == ClaimTypes.Name);
var identityKey = _signInManager.Context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName];
_cache.Set($"{userId.Value}:{identityKey}", identityKey, new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTime.Now.AddMinutes(ConfigureCookieSettings.ValidityMinutesPeriod)
});
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
else
{
return RedirectToPage("/Index");
}
}
}
@@ -0,0 +1,41 @@
@page
@model RegisterModel
@{
ViewData["Title"] = "Register";
}
<div class="container">
<h2>@ViewData["Title"]</h2>
<div class="row">
<div class="col-md-8">
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
<h4>Create a new account.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.ConfirmPassword"></label>
<input asp-for="Input.ConfirmPassword" class="form-control" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
</form>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.Extensions.Logging;
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account;
[AllowAnonymous]
public class RegisterModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
}
[BindProperty]
public required InputModel Input { get; set; }
public string? ReturnUrl { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string? Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string? Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string? ConfirmPassword { get; set; }
}
public void OnGet(string? returnUrl = null)
{
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = Input?.Email, Email = Input?.Email };
var result = await _userManager.CreateAsync(user, Input?.Password!);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);
Guard.Against.Null(callbackUrl, nameof(callbackUrl));
await _emailSender.SendEmailAsync(Input!.Email!, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
// If we got this far, something failed, redisplay form
return Page();
}
}
@@ -0,0 +1 @@
@using mediasrv.Web.Areas.Identity.Pages.Account
@@ -0,0 +1,18 @@
<environment include="Development,Docker">
<script src="~/Identity/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
<environment exclude="Development,Docker">
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.17.0/jquery.validate.min.js"
asp-fallback-src="~/Identity/lib/jquery-validation/dist/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator"
crossorigin="anonymous"
integrity="sha384-rZfj/ogBloos6wzLGpPkkOr/gpkBNLZ6b6yLy4o+ok+t/SAKlL5mvXLr0OXNi1Hp">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.9/jquery.validate.unobtrusive.min.js"
asp-fallback-src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
crossorigin="anonymous"
integrity="sha384-ifv0TYDWxBHzvAk2Z0n8R434FL1Rlv/Av18DXE43N/1rvHyOG4izKst0f2iSLdds">
</script>
</environment>
@@ -0,0 +1,5 @@
@using Microsoft.AspNetCore.Identity
@using Microsoft.eShopWeb.Web.Areas.Identity
@using Microsoft.eShopWeb.Infrastructure.Identity
@namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@@ -0,0 +1,3 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}