From d63c54b3ee7e09e181f1ed70693741529ca157d1 Mon Sep 17 00:00:00 2001 From: Chalez Date: Mon, 12 Feb 2024 00:45:16 -0500 Subject: [PATCH] Adding initial login code --- .../Constants/AuthorizationConstants.cs | 12 + .../BlazorAdmin/CustomAuthStateProvidor.cs | 86 ++++++ .../BlazorAdmin/Helpers/BlazorComponent.cs | 25 ++ .../Helpers/BlazorLayoutComponent.cs | 24 ++ .../BlazorAdmin/Helpers/RefreshBroadcast.cs | 23 ++ .../src/BlazorAdmin/Helpers/ToastComponent.cs | 87 ++++++ mediasrv/src/BlazorAdmin/Pages/Logout.razor | 14 + mediasrv/src/BlazorShared/ClaimValue.cs | 17 ++ mediasrv/src/BlazorShared/Constants.cs | 9 + mediasrv/src/BlazorShared/UserInfo.cs | 13 + .../Identity/AppIdentityDbContext.cs | 21 ++ .../Identity/AppIdentityDbContextSeed.cs | 32 ++ .../Identity/ApplicationUser.cs | 7 + .../Identity/IdentityTokenClaimService.cs | 47 +++ ...202111612_InitialIdentityModel.Designer.cs | 273 ++++++++++++++++++ .../20201202111612_InitialIdentityModel.cs | 218 ++++++++++++++ .../AppIdentityDbContextModelSnapshot.cs | 271 +++++++++++++++++ .../Identity/UserNotFoundException.cs | 10 + ...uthenticateEndpoint.AuthenticateRequest.cs | 7 + ...thenticateEndpoint.AuthenticateResponse.cs | 20 ++ .../AuthenticateEndpoint.ClaimValue.cs | 17 ++ .../AuthenticateEndpoint.UserInfo.cs | 12 + .../AuthEndpoints/AuthenticateEndpoint.cs | 59 ++++ .../Middleware/ExceptionMiddleware.cs | 54 ++++ mediasrv/src/Shared/CustomInputSelect.cs | 37 +++ mediasrv/src/Shared/MainLayout.razor | 41 +++ mediasrv/src/Shared/NavMenu.razor | 44 +++ mediasrv/src/Shared/RedirectToLogin.razor | 12 + mediasrv/src/Shared/Spinner.razor | 9 + mediasrv/src/Shared/Toast.razor | 13 + .../Areas/Identity/IdentityHostingStartup.cs | 14 + .../Pages/Account/ConfirmEmail.cshtml | 12 + .../Pages/Account/ConfirmEmail.cshtml.cs | 44 +++ .../Areas/Identity/Pages/Account/Login.cshtml | 63 ++++ .../Identity/Pages/Account/Login.cshtml.cs | 119 ++++++++ .../Identity/Pages/Account/Logout.cshtml | 10 + .../Identity/Pages/Account/Logout.cshtml.cs | 52 ++++ .../Identity/Pages/Account/Register.cshtml | 41 +++ .../Identity/Pages/Account/Register.cshtml.cs | 100 +++++++ .../Pages/Account/_ViewImports.cshtml | 1 + .../Pages/_ValidationScriptsPartial.cshtml | 18 ++ .../Areas/Identity/Pages/_ViewImports.cshtml | 5 + .../Areas/Identity/Pages/_ViewStart.cshtml | 3 + 43 files changed, 1996 insertions(+) create mode 100644 mediasrv/src/ApplicationCore/Constants/AuthorizationConstants.cs create mode 100644 mediasrv/src/BlazorAdmin/CustomAuthStateProvidor.cs create mode 100644 mediasrv/src/BlazorAdmin/Helpers/BlazorComponent.cs create mode 100644 mediasrv/src/BlazorAdmin/Helpers/BlazorLayoutComponent.cs create mode 100644 mediasrv/src/BlazorAdmin/Helpers/RefreshBroadcast.cs create mode 100644 mediasrv/src/BlazorAdmin/Helpers/ToastComponent.cs create mode 100644 mediasrv/src/BlazorAdmin/Pages/Logout.razor create mode 100644 mediasrv/src/BlazorShared/ClaimValue.cs create mode 100644 mediasrv/src/BlazorShared/Constants.cs create mode 100644 mediasrv/src/BlazorShared/UserInfo.cs create mode 100644 mediasrv/src/Infrastructure/Identity/AppIdentityDbContext.cs create mode 100644 mediasrv/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs create mode 100644 mediasrv/src/Infrastructure/Identity/ApplicationUser.cs create mode 100644 mediasrv/src/Infrastructure/Identity/IdentityTokenClaimService.cs create mode 100644 mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.Designer.cs create mode 100644 mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.cs create mode 100644 mediasrv/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs create mode 100644 mediasrv/src/Infrastructure/Identity/UserNotFoundException.cs create mode 100644 mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs create mode 100644 mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs create mode 100644 mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs create mode 100644 mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs create mode 100644 mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs create mode 100644 mediasrv/src/PublicApi/Middleware/ExceptionMiddleware.cs create mode 100644 mediasrv/src/Shared/CustomInputSelect.cs create mode 100644 mediasrv/src/Shared/MainLayout.razor create mode 100644 mediasrv/src/Shared/NavMenu.razor create mode 100644 mediasrv/src/Shared/RedirectToLogin.razor create mode 100644 mediasrv/src/Shared/Spinner.razor create mode 100644 mediasrv/src/Shared/Toast.razor create mode 100644 mediasrv/src/Web/Areas/Identity/IdentityHostingStartup.cs create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/_ViewImports.cshtml create mode 100644 mediasrv/src/Web/Areas/Identity/Pages/_ViewStart.cshtml diff --git a/mediasrv/src/ApplicationCore/Constants/AuthorizationConstants.cs b/mediasrv/src/ApplicationCore/Constants/AuthorizationConstants.cs new file mode 100644 index 0000000..3528d27 --- /dev/null +++ b/mediasrv/src/ApplicationCore/Constants/AuthorizationConstants.cs @@ -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"; +} diff --git a/mediasrv/src/BlazorAdmin/CustomAuthStateProvidor.cs b/mediasrv/src/BlazorAdmin/CustomAuthStateProvidor.cs new file mode 100644 index 0000000..54a83a9 --- /dev/null +++ b/mediasrv/src/BlazorAdmin/CustomAuthStateProvidor.cs @@ -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 _logger; + + private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0); + private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity()); + + public CustomAuthStateProvider(HttpClient httpClient, + ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public override async Task GetAuthenticationStateAsync() + { + return new AuthenticationState(await GetUser(useCache: true)); + } + + private async ValueTask 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 FetchUser() + { + UserInfo user = null; + + try + { + _logger.LogInformation("Fetching user details from web api."); + user = await _httpClient.GetFromJsonAsync("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); + } +} diff --git a/mediasrv/src/BlazorAdmin/Helpers/BlazorComponent.cs b/mediasrv/src/BlazorAdmin/Helpers/BlazorComponent.cs new file mode 100644 index 0000000..36d7141 --- /dev/null +++ b/mediasrv/src/BlazorAdmin/Helpers/BlazorComponent.cs @@ -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(); + } + +} diff --git a/mediasrv/src/BlazorAdmin/Helpers/BlazorLayoutComponent.cs b/mediasrv/src/BlazorAdmin/Helpers/BlazorLayoutComponent.cs new file mode 100644 index 0000000..e628832 --- /dev/null +++ b/mediasrv/src/BlazorAdmin/Helpers/BlazorLayoutComponent.cs @@ -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(); + } +} diff --git a/mediasrv/src/BlazorAdmin/Helpers/RefreshBroadcast.cs b/mediasrv/src/BlazorAdmin/Helpers/RefreshBroadcast.cs new file mode 100644 index 0000000..a31e96c --- /dev/null +++ b/mediasrv/src/BlazorAdmin/Helpers/RefreshBroadcast.cs @@ -0,0 +1,23 @@ +using System; + +namespace BlazorAdmin.Helpers; + +internal sealed class RefreshBroadcast +{ + private static readonly Lazy + Lazy = + new Lazy + (() => new RefreshBroadcast()); + + public static RefreshBroadcast Instance => Lazy.Value; + + private RefreshBroadcast() + { + } + + public event Action RefreshRequested; + public void CallRequestRefresh() + { + RefreshRequested?.Invoke(); + } +} diff --git a/mediasrv/src/BlazorAdmin/Helpers/ToastComponent.cs b/mediasrv/src/BlazorAdmin/Helpers/ToastComponent.cs new file mode 100644 index 0000000..33fd667 --- /dev/null +++ b/mediasrv/src/BlazorAdmin/Helpers/ToastComponent.cs @@ -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; + } +} diff --git a/mediasrv/src/BlazorAdmin/Pages/Logout.razor b/mediasrv/src/BlazorAdmin/Pages/Logout.razor new file mode 100644 index 0000000..ada679c --- /dev/null +++ b/mediasrv/src/BlazorAdmin/Pages/Logout.razor @@ -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"); + } + +} diff --git a/mediasrv/src/BlazorShared/ClaimValue.cs b/mediasrv/src/BlazorShared/ClaimValue.cs new file mode 100644 index 0000000..6ff657e --- /dev/null +++ b/mediasrv/src/BlazorShared/ClaimValue.cs @@ -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; } +} diff --git a/mediasrv/src/BlazorShared/Constants.cs b/mediasrv/src/BlazorShared/Constants.cs new file mode 100644 index 0000000..58a5e45 --- /dev/null +++ b/mediasrv/src/BlazorShared/Constants.cs @@ -0,0 +1,9 @@ +namespace BlazorShared.Authorization; + +public static class Constants +{ + public static class Roles + { + public const string ADMINISTRATORS = "Administrators"; + } +} diff --git a/mediasrv/src/BlazorShared/UserInfo.cs b/mediasrv/src/BlazorShared/UserInfo.cs new file mode 100644 index 0000000..7ad76c0 --- /dev/null +++ b/mediasrv/src/BlazorShared/UserInfo.cs @@ -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 Claims { get; set; } +} diff --git a/mediasrv/src/Infrastructure/Identity/AppIdentityDbContext.cs b/mediasrv/src/Infrastructure/Identity/AppIdentityDbContext.cs new file mode 100644 index 0000000..638defc --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/AppIdentityDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + + +namespace mediasrv.Infrastructure.Identity; + +public class AppIdentityDbContext : IdentityDbContext +{ + public AppIdentityDbContext(DbContextOptions 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); + } +} diff --git a/mediasrv/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs b/mediasrv/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs new file mode 100644 index 0000000..ba4b732 --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs @@ -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 userManager, RoleManager 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); + } + } +} diff --git a/mediasrv/src/Infrastructure/Identity/ApplicationUser.cs b/mediasrv/src/Infrastructure/Identity/ApplicationUser.cs new file mode 100644 index 0000000..bcc80b6 --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/ApplicationUser.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Identity; + +namespace mediasrv.Infrastructure.Identity; + +public class ApplicationUser : IdentityUser +{ +} diff --git a/mediasrv/src/Infrastructure/Identity/IdentityTokenClaimService.cs b/mediasrv/src/Infrastructure/Identity/IdentityTokenClaimService.cs new file mode 100644 index 0000000..0ae30fe --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/IdentityTokenClaimService.cs @@ -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 _userManager; + + public IdentityTokenClaimService(UserManager userManager) + { + _userManager = userManager; + } + + public async Task 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 { 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); + } +} diff --git a/mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.Designer.cs b/mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.Designer.cs new file mode 100644 index 0000000..89ddc99 --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.Designer.cs @@ -0,0 +1,273 @@ +// +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("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"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .UseIdentityColumn(); + + 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"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .UseIdentityColumn(); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", 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"); + }); + + 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.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.cs b/mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.cs new file mode 100644 index 0000000..d98aeeb --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.cs @@ -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(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/mediasrv/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs b/mediasrv/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs new file mode 100644 index 0000000..9582d72 --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs @@ -0,0 +1,271 @@ +// +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("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"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .UseIdentityColumn(); + + 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"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .UseIdentityColumn(); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", 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"); + }); + + 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.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/mediasrv/src/Infrastructure/Identity/UserNotFoundException.cs b/mediasrv/src/Infrastructure/Identity/UserNotFoundException.cs new file mode 100644 index 0000000..6bb7a01 --- /dev/null +++ b/mediasrv/src/Infrastructure/Identity/UserNotFoundException.cs @@ -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}") + { + } +} diff --git a/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs new file mode 100644 index 0000000..6876427 --- /dev/null +++ b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs @@ -0,0 +1,7 @@ +namespace mediasrv.PublicApi.AuthEndpoints; + +public class AuthenticateRequest : BaseRequest +{ + public string Username { get; set; } + public string Password { get; set; } +} diff --git a/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs new file mode 100644 index 0000000..5a29c63 --- /dev/null +++ b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs @@ -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; +} diff --git a/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs new file mode 100644 index 0000000..8c22f30 --- /dev/null +++ b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs @@ -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; +} diff --git a/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs new file mode 100644 index 0000000..e232656 --- /dev/null +++ b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs @@ -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 Claims { get; set; } = new List(); +} diff --git a/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs new file mode 100644 index 0000000..1ffc15f --- /dev/null +++ b/mediasrv/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs @@ -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; + +/// +/// Authenticates a user +/// +public class AuthenticateEndpoint : EndpointBaseAsync + .WithRequest + .WithActionResult +{ + private readonly SignInManager _signInManager; + private readonly ITokenClaimsService _tokenClaimsService; + + public AuthenticateEndpoint(SignInManager 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> 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; + } +} diff --git a/mediasrv/src/PublicApi/Middleware/ExceptionMiddleware.cs b/mediasrv/src/PublicApi/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000..226e21a --- /dev/null +++ b/mediasrv/src/PublicApi/Middleware/ExceptionMiddleware.cs @@ -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()); + } + } +} diff --git a/mediasrv/src/Shared/CustomInputSelect.cs b/mediasrv/src/Shared/CustomInputSelect.cs new file mode 100644 index 0000000..eb39331 --- /dev/null +++ b/mediasrv/src/Shared/CustomInputSelect.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Components.Forms; + +namespace BlazorAdmin.Shared; + +/// +/// This is needed until 5.0 ships with native support +/// https://www.pragimtech.com/blog/blazor/inputselect-does-not-support-system.int32/ +/// +/// +public class CustomInputSelect : InputSelect +{ + 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); + } + } +} diff --git a/mediasrv/src/Shared/MainLayout.razor b/mediasrv/src/Shared/MainLayout.razor new file mode 100644 index 0000000..a7d8383 --- /dev/null +++ b/mediasrv/src/Shared/MainLayout.razor @@ -0,0 +1,41 @@ +@inject AuthenticationStateProvider AuthStateProvider +@inject IJSRuntime JSRuntime + +@inherits BlazorAdmin.Helpers.BlazorLayoutComponent + + + + + + +
+ + + +
+ + @Body +
+
+ @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); + } + } diff --git a/mediasrv/src/Shared/NavMenu.razor b/mediasrv/src/Shared/NavMenu.razor new file mode 100644 index 0000000..49c44cd --- /dev/null +++ b/mediasrv/src/Shared/NavMenu.razor @@ -0,0 +1,44 @@ +@inherits BlazorAdmin.Helpers.BlazorComponent + + +
+ +
+ +@code { + + private bool collapseNavMenu = true; + + private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} diff --git a/mediasrv/src/Shared/RedirectToLogin.razor b/mediasrv/src/Shared/RedirectToLogin.razor new file mode 100644 index 0000000..de9c49d --- /dev/null +++ b/mediasrv/src/Shared/RedirectToLogin.razor @@ -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}"); + } +} \ No newline at end of file diff --git a/mediasrv/src/Shared/Spinner.razor b/mediasrv/src/Shared/Spinner.razor new file mode 100644 index 0000000..29ed31e --- /dev/null +++ b/mediasrv/src/Shared/Spinner.razor @@ -0,0 +1,9 @@ + +@inherits BlazorAdmin.Helpers.BlazorComponent + +@namespace BlazorAdmin.Shared + +
+ Loading... +
+ diff --git a/mediasrv/src/Shared/Toast.razor b/mediasrv/src/Shared/Toast.razor new file mode 100644 index 0000000..f316d6a --- /dev/null +++ b/mediasrv/src/Shared/Toast.razor @@ -0,0 +1,13 @@ +@inherits BlazorAdmin.Helpers.ToastComponent + +@namespace BlazorAdmin.Shared + +
+
+ +
+
+
@Heading
+

@Message

+
+
diff --git a/mediasrv/src/Web/Areas/Identity/IdentityHostingStartup.cs b/mediasrv/src/Web/Areas/Identity/IdentityHostingStartup.cs new file mode 100644 index 0000000..cf6c773 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/IdentityHostingStartup.cs @@ -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) => + { + }); + } +} diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000..401bf32 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml @@ -0,0 +1,12 @@ +@page +@model ConfirmEmailModel +@{ + ViewData["Title"] = "Confirm email"; +} + +

@ViewData["Title"]

+
+

+ Thank you for confirming your email. +

+
diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs new file mode 100644 index 0000000..28a0d0c --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs @@ -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 _userManager; + + public ConfirmEmailModel(UserManager userManager) + { + _userManager = userManager; + } + + public async Task 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(); + } +} diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 0000000..c512527 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,63 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + + + + +@section Scripts { + +} diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..062db02 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -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 _signInManager; + private readonly ILogger _logger; + private readonly IBasketService _basketService; + + public LoginModel(SignInManager signInManager, ILogger logger, IBasketService basketService) + { + _signInManager = signInManager; + _logger = logger; + _basketService = basketService; + } + + [BindProperty] + public required InputModel Input { get; set; } + + public IList? 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 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); + } + } +} diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml new file mode 100644 index 0000000..cb864ef --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml @@ -0,0 +1,10 @@ +@page +@model LogoutModel +@{ + ViewData["Title"] = "Log out"; +} + +
+

@ViewData["Title"]

+

You have successfully logged out of the application.

+
\ No newline at end of file diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs b/mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs new file mode 100644 index 0000000..80bb467 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs @@ -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 _signInManager; + private readonly ILogger _logger; + private readonly IMemoryCache _cache; + + public LogoutModel(SignInManager signInManager, ILogger logger, IMemoryCache cache) + { + _signInManager = signInManager; + _logger = logger; + _cache = cache; + } + + public void OnGet() + { + } + + public async Task 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"); + } + } +} diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml new file mode 100644 index 0000000..cc6783b --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml @@ -0,0 +1,41 @@ +@page +@model RegisterModel +@{ + ViewData["Title"] = "Register"; +} + +
+ +

@ViewData["Title"]

+ +
+
+
+

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +
+ +@section Scripts { + +} diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs b/mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs new file mode 100644 index 0000000..f0165fa --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -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 _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + + public RegisterModel( + UserManager userManager, + SignInManager signInManager, + ILogger 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 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 clicking here."); + + 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(); + } +} diff --git a/mediasrv/src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000..7aa25d3 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using mediasrv.Web.Areas.Identity.Pages.Account \ No newline at end of file diff --git a/mediasrv/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..ea2a0df --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/mediasrv/src/Web/Areas/Identity/Pages/_ViewImports.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..82c4f63 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/_ViewImports.cshtml @@ -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 diff --git a/mediasrv/src/Web/Areas/Identity/Pages/_ViewStart.cshtml b/mediasrv/src/Web/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..c4284f6 --- /dev/null +++ b/mediasrv/src/Web/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +}