From ff3bc47432cee315da56ee12c3fe871339c25e74 Mon Sep 17 00:00:00 2001 From: Vadim <92179377+Zhuzhalica@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:45:28 +0500 Subject: [PATCH 1/7] task 1 done --- .../Areas/Identity/Data/PhotosAppUser.cs | 13 + .../Areas/Identity/Data/UsersDbContext.cs | 28 ++ .../Areas/Identity/IdentityHostingStartup.cs | 49 ++++ .../Pages/Account/AccessDenied.cshtml | 10 + .../Pages/Account/AccessDenied.cshtml.cs | 17 ++ .../Pages/Account/ConfirmEmail.cshtml | 7 + .../Pages/Account/ConfirmEmail.cshtml.cs | 47 +++ .../Pages/Account/ConfirmEmailChange.cshtml | 8 + .../Account/ConfirmEmailChange.cshtml.cs | 65 +++++ .../Pages/Account/ExternalLogin.cshtml | 33 +++ .../Pages/Account/ExternalLogin.cshtml.cs | 169 +++++++++++ .../Pages/Account/ForgotPassword.cshtml | 26 ++ .../Pages/Account/ForgotPassword.cshtml.cs | 71 +++++ .../Account/ForgotPasswordConfirmation.cshtml | 11 + .../ForgotPasswordConfirmation.cshtml.cs | 16 ++ .../Identity/Pages/Account/Lockout.cshtml | 10 + .../Identity/Pages/Account/Lockout.cshtml.cs | 18 ++ .../Areas/Identity/Pages/Account/Login.cshtml | 85 ++++++ .../Identity/Pages/Account/Login.cshtml.cs | 111 +++++++ .../Pages/Account/LoginWith2fa.cshtml | 41 +++ .../Pages/Account/LoginWith2fa.cshtml.cs | 99 +++++++ .../Account/LoginWithRecoveryCode.cshtml | 29 ++ .../Account/LoginWithRecoveryCode.cshtml.cs | 90 ++++++ .../Identity/Pages/Account/Logout.cshtml | 21 ++ .../Identity/Pages/Account/Logout.cshtml.cs | 44 +++ .../Account/Manage/ChangePassword.cshtml | 36 +++ .../Account/Manage/ChangePassword.cshtml.cs | 101 +++++++ .../Account/Manage/DeletePersonalData.cshtml | 33 +++ .../Manage/DeletePersonalData.cshtml.cs | 84 ++++++ .../Pages/Account/Manage/Disable2fa.cshtml | 25 ++ .../Pages/Account/Manage/Disable2fa.cshtml.cs | 64 +++++ .../Manage/DownloadPersonalData.cshtml | 12 + .../Manage/DownloadPersonalData.cshtml.cs | 57 ++++ .../Pages/Account/Manage/Email.cshtml | 43 +++ .../Pages/Account/Manage/Email.cshtml.cs | 148 ++++++++++ .../Account/Manage/EnableAuthenticator.cshtml | 53 ++++ .../Manage/EnableAuthenticator.cshtml.cs | 157 ++++++++++ .../Account/Manage/ExternalLogins.cshtml | 53 ++++ .../Account/Manage/ExternalLogins.cshtml.cs | 110 +++++++ .../Manage/GenerateRecoveryCodes.cshtml | 27 ++ .../Manage/GenerateRecoveryCodes.cshtml.cs | 73 +++++ .../Pages/Account/Manage/Index.cshtml | 30 ++ .../Pages/Account/Manage/Index.cshtml.cs | 96 +++++++ .../Pages/Account/Manage/ManageNavPages.cs | 50 ++++ .../Pages/Account/Manage/PersonalData.cshtml | 27 ++ .../Account/Manage/PersonalData.cshtml.cs | 34 +++ .../Account/Manage/ResetAuthenticator.cshtml | 24 ++ .../Manage/ResetAuthenticator.cshtml.cs | 61 ++++ .../Pages/Account/Manage/SetPassword.cshtml | 35 +++ .../Account/Manage/SetPassword.cshtml.cs | 93 ++++++ .../Account/Manage/ShowRecoveryCodes.cshtml | 25 ++ .../Manage/ShowRecoveryCodes.cshtml.cs | 31 ++ .../Manage/TwoFactorAuthentication.cshtml | 57 ++++ .../Manage/TwoFactorAuthentication.cshtml.cs | 72 +++++ .../Pages/Account/Manage/_Layout.cshtml | 29 ++ .../Pages/Account/Manage/_ManageNav.cshtml | 15 + .../Account/Manage/_StatusMessage.cshtml | 10 + .../Pages/Account/Manage/_ViewImports.cshtml | 1 + .../Identity/Pages/Account/Register.cshtml | 67 +++++ .../Identity/Pages/Account/Register.cshtml.cs | 115 ++++++++ .../Pages/Account/RegisterConfirmation.cshtml | 22 ++ .../Account/RegisterConfirmation.cshtml.cs | 62 ++++ .../Account/ResendEmailConfirmation.cshtml | 26 ++ .../Account/ResendEmailConfirmation.cshtml.cs | 74 +++++ .../Pages/Account/ResetPassword.cshtml | 37 +++ .../Pages/Account/ResetPassword.cshtml.cs | 91 ++++++ .../Account/ResetPasswordConfirmation.cshtml | 10 + .../ResetPasswordConfirmation.cshtml.cs | 18 ++ .../Pages/Account/_StatusMessage.cshtml | 10 + .../Pages/Account/_ViewImports.cshtml | 1 + PhotosApp/Areas/Identity/Pages/Error.cshtml | 23 ++ .../Areas/Identity/Pages/Error.cshtml.cs | 21 ++ .../Pages/_ValidationScriptsPartial.cshtml | 18 ++ .../Areas/Identity/Pages/_ViewImports.cshtml | 5 + .../Areas/Identity/Pages/_ViewStart.cshtml | 4 + PhotosApp/Controllers/PhotosController.cs | 6 +- PhotosApp/Data/PhotosAppDataExtensions.cs | 5 + .../UsersDb/20231109075354_Users.Designer.cs | 270 ++++++++++++++++++ .../UsersDb/20231109075354_Users.cs | 217 ++++++++++++++ .../UsersDb/UsersDbContextModelSnapshot.cs | 268 +++++++++++++++++ PhotosApp/ScaffoldingReadMe.txt | 3 + PhotosApp/Services/SimplePasswordHasher.cs | 11 +- PhotosApp/Startup.cs | 13 +- PhotosApp/Views/Shared/_Layout.cshtml | 5 + PhotosApp/Views/Shared/_LoginPartial.cshtml | 28 ++ PhotosApp/appsettings.json | 5 +- 86 files changed, 4312 insertions(+), 7 deletions(-) create mode 100644 PhotosApp/Areas/Identity/Data/PhotosAppUser.cs create mode 100644 PhotosApp/Areas/Identity/Data/UsersDbContext.cs create mode 100644 PhotosApp/Areas/Identity/IdentityHostingStartup.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Login.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Login.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Logout.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Logout.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/_Layout.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Register.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/Register.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/Account/_StatusMessage.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Account/_ViewImports.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Error.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/Error.cshtml.cs create mode 100644 PhotosApp/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/_ViewImports.cshtml create mode 100644 PhotosApp/Areas/Identity/Pages/_ViewStart.cshtml create mode 100644 PhotosApp/Migrations/UsersDb/20231109075354_Users.Designer.cs create mode 100644 PhotosApp/Migrations/UsersDb/20231109075354_Users.cs create mode 100644 PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs create mode 100644 PhotosApp/ScaffoldingReadMe.txt create mode 100644 PhotosApp/Views/Shared/_LoginPartial.cshtml diff --git a/PhotosApp/Areas/Identity/Data/PhotosAppUser.cs b/PhotosApp/Areas/Identity/Data/PhotosAppUser.cs new file mode 100644 index 00000000..730cd6fd --- /dev/null +++ b/PhotosApp/Areas/Identity/Data/PhotosAppUser.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace PhotosApp.Areas.Identity.Data +{ + // Add profile data for application users by adding properties to the PhotosAppUser class + public class PhotosAppUser : IdentityUser + { + } +} diff --git a/PhotosApp/Areas/Identity/Data/UsersDbContext.cs b/PhotosApp/Areas/Identity/Data/UsersDbContext.cs new file mode 100644 index 00000000..5d0f3a85 --- /dev/null +++ b/PhotosApp/Areas/Identity/Data/UsersDbContext.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Data +{ + public class UsersDbContext : IdentityDbContext + { + public UsersDbContext(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/PhotosApp/Areas/Identity/IdentityHostingStartup.cs b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs new file mode 100644 index 00000000..65cf4d78 --- /dev/null +++ b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using PhotosApp.Areas.Identity.Data; +using PhotosApp.Services; + +[assembly: HostingStartup(typeof(PhotosApp.Areas.Identity.IdentityHostingStartup))] +namespace PhotosApp.Areas.Identity +{ + public class IdentityHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => { + services.AddDbContext(options => + options.UseSqlite( + context.Configuration.GetConnectionString("UsersDbContextConnection"))); + + services.AddDefaultIdentity() + .AddPasswordValidator>() + .AddErrorDescriber() + .AddEntityFrameworkStores(); + + services.Configure(options => + { + options.Password.RequireDigit = false; + options.Password.RequireLowercase = true; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequiredLength = 6; + options.Password.RequiredUniqueChars = 1; + + options.SignIn.RequireConfirmedAccount = false; + }); + + services.Configure(options => + { + options.CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV3; + options.IterationCount = 12000; + }); + + }); + } + } +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml b/PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml new file mode 100644 index 00000000..017f6ff4 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml @@ -0,0 +1,10 @@ +@page +@model AccessDeniedModel +@{ + ViewData["Title"] = "Access denied"; +} + +
+

@ViewData["Title"]

+

You do not have access to this resource.

+
diff --git a/PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs new file mode 100644 index 00000000..9b9a5598 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + public class AccessDeniedModel : PageModel + { + public void OnGet() + { + + } + } +} + diff --git a/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml new file mode 100644 index 00000000..26deba20 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml @@ -0,0 +1,7 @@ +@page +@model ConfirmEmailModel +@{ + ViewData["Title"] = "Confirm email"; +} + +

@ViewData["Title"]

\ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs new file mode 100644 index 00000000..2ac96d49 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ConfirmEmailModel : PageModel + { + private readonly UserManager _userManager; + + public ConfirmEmailModel(UserManager userManager) + { + _userManager = userManager; + } + + [TempData] + public string StatusMessage { get; set; } + + 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}'."); + } + + code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); + var result = await _userManager.ConfirmEmailAsync(user, code); + StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml new file mode 100644 index 00000000..98d57c88 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml @@ -0,0 +1,8 @@ +@page +@model ConfirmEmailChangeModel +@{ + ViewData["Title"] = "Confirm email change"; +} + +

@ViewData["Title"]

+ \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs new file mode 100644 index 00000000..5cfa130b --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ConfirmEmailChangeModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ConfirmEmailChangeModel(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync(string userId, string email, string code) + { + if (userId == null || email == null || code == null) + { + return RedirectToPage("/Index"); + } + + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return NotFound($"Unable to load user with ID '{userId}'."); + } + + code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); + var result = await _userManager.ChangeEmailAsync(user, email, code); + if (!result.Succeeded) + { + StatusMessage = "Error changing email."; + return Page(); + } + + // In our UI email and user name are one and the same, so when we update the email + // we need to update the user name. + var setUserNameResult = await _userManager.SetUserNameAsync(user, email); + if (!setUserNameResult.Succeeded) + { + StatusMessage = "Error changing user name."; + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Thank you for confirming your email change."; + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml new file mode 100644 index 00000000..f7dc967d --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml @@ -0,0 +1,33 @@ +@page +@model ExternalLoginModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+

Associate your @Model.ProviderDisplayName account.

+
+ +

+ You've successfully authenticated with @Model.ProviderDisplayName. + Please enter an email address for this site below and click the Register button to finish + logging in. +

+ +
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs new file mode 100644 index 00000000..709e895f --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +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.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ExternalLoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + + public ExternalLoginModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger, + IEmailSender emailSender) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ProviderDisplayName { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public IActionResult OnGetAsync() + { + return RedirectToPage("./Login"); + } + + public IActionResult OnPost(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetCallbackAsync(string returnUrl = null, string remoteError = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + if (remoteError != null) + { + ErrorMessage = $"Error from external provider: {remoteError}"; + return RedirectToPage("./Login", new {ReturnUrl = returnUrl }); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + ErrorMessage = "Error loading external login information."; + return RedirectToPage("./Login", new { ReturnUrl = returnUrl }); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); + if (result.Succeeded) + { + _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); + return LocalRedirect(returnUrl); + } + if (result.IsLockedOut) + { + return RedirectToPage("./Lockout"); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ReturnUrl = returnUrl; + ProviderDisplayName = info.ProviderDisplayName; + if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) + { + Input = new InputModel + { + Email = info.Principal.FindFirstValue(ClaimTypes.Email) + }; + } + return Page(); + } + } + + public async Task OnPostConfirmationAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + // Get the information about the user from the external login provider + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + ErrorMessage = "Error loading external login information during confirmation."; + return RedirectToPage("./Login", new { ReturnUrl = returnUrl }); + } + + if (ModelState.IsValid) + { + var user = new PhotosAppUser { UserName = Input.Email, Email = Input.Email }; + + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) + { + result = await _userManager.AddLoginAsync(user, info); + if (result.Succeeded) + { + _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); + + var userId = await _userManager.GetUserIdAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = userId, code = code }, + protocol: Request.Scheme); + + await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", + $"Please confirm your account by clicking here."); + + // If account confirmation is required, we need to show the link if we don't have a real email sender + if (_userManager.Options.SignIn.RequireConfirmedAccount) + { + return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email }); + } + + await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider); + + return LocalRedirect(returnUrl); + } + } + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + ProviderDisplayName = info.ProviderDisplayName; + ReturnUrl = returnUrl; + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml new file mode 100644 index 00000000..94f46b28 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml @@ -0,0 +1,26 @@ +@page +@model ForgotPasswordModel +@{ + ViewData["Title"] = "Forgot your password?"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs new file mode 100644 index 00000000..8618858f --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Encodings.Web; +using System.Text; +using System.Threading.Tasks; +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.AspNetCore.WebUtilities; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ForgotPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + + public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) + { + _userManager = userManager; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public async Task OnPostAsync() + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await _userManager.GeneratePasswordResetTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ResetPassword", + pageHandler: null, + values: new { area = "Identity", code }, + protocol: Request.Scheme); + + await _emailSender.SendEmailAsync( + Input.Email, + "Reset Password", + $"Please reset your password by clicking here."); + + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 00000000..1a1b7f96 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,11 @@ +@page +@model ForgotPasswordConfirmation +@{ + ViewData["Title"] = "Forgot password confirmation"; +} + +

@ViewData["Title"]

+

+ Please check your email to reset your password. +

+ diff --git a/PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs new file mode 100644 index 00000000..e0066bf9 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ForgotPasswordConfirmation : PageModel + { + public void OnGet() + { + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml new file mode 100644 index 00000000..4eded882 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml @@ -0,0 +1,10 @@ +@page +@model LockoutModel +@{ + ViewData["Title"] = "Locked out"; +} + +
+

@ViewData["Title"]

+

This account has been locked out, please try again later.

+
diff --git a/PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml.cs new file mode 100644 index 00000000..0c07ffc7 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Lockout.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LockoutModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Login.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 00000000..72a567fa --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,85 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + +

@ViewData["Title"]

+
+
+
+
+

Use a local account to log in.

+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Login.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 00000000..77979f84 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, + ILogger logger, + UserManager userManager) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList ExternalLogins { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required(ErrorMessage = "Email - обязательное поле")] + [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 ??= 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 ??= Url.Content("~/"); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + 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: false); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + 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(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml b/PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml new file mode 100644 index 00000000..780b4ec3 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml @@ -0,0 +1,41 @@ +@page +@model LoginWith2faModel +@{ + ViewData["Title"] = "Two-factor authentication"; +} + +

@ViewData["Title"]

+
+

Your login is protected with an authenticator app. Enter your authenticator code below.

+
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+ +
+
+
+
+

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs new file mode 100644 index 00000000..7f2576d0 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +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 Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginWith2faModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWith2faModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public bool RememberMe { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + } + + public async Task OnGetAsync(bool rememberMe, string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + RememberMe = rememberMe; + + return Page(); + } + + public async Task OnPostAsync(bool rememberMe, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + returnUrl = returnUrl ?? Url.Content("~/"); + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); + + var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id); + return LocalRedirect(returnUrl); + } + else if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id); + ModelState.AddModelError(string.Empty, "Invalid authenticator code."); + return Page(); + } + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml b/PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml new file mode 100644 index 00000000..d866adb3 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml @@ -0,0 +1,29 @@ +@page +@model LoginWithRecoveryCodeModel +@{ + ViewData["Title"] = "Recovery code verification"; +} + +

@ViewData["Title"]

+
+

+ You have requested to log in with a recovery code. This login will not be remembered until you provide + an authenticator app code at log in or disable 2FA and log in again. +

+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs new file mode 100644 index 00000000..0c7165e8 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +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 Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginWithRecoveryCodeModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWithRecoveryCodeModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [BindProperty] + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + + return Page(); + } + + public async Task OnPostAsync(string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); + + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id); + return LocalRedirect(returnUrl ?? Url.Content("~/")); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id); + ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); + return Page(); + } + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Logout.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Logout.cshtml new file mode 100644 index 00000000..eca33c64 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Logout.cshtml @@ -0,0 +1,21 @@ +@page +@model LogoutModel +@{ + ViewData["Title"] = "Log out"; +} + +
+

@ViewData["Title"]

+ @{ + if (User.Identity.IsAuthenticated) + { +
+ +
+ } + else + { +

You have successfully logged out of the application.

+ } + } +
\ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Logout.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Logout.cshtml.cs new file mode 100644 index 00000000..7457918e --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Logout.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 Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LogoutModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LogoutModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + public void OnGet() + { + } + + public async Task OnPost(string returnUrl = null) + { + await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out."); + if (returnUrl != null) + { + return LocalRedirect(returnUrl); + } + else + { + return RedirectToPage(); + } + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml new file mode 100644 index 00000000..31a2ea59 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml @@ -0,0 +1,36 @@ +@page +@model ChangePasswordModel +@{ + ViewData["Title"] = "Change password"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs new file mode 100644 index 00000000..37206c12 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class ChangePasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public ChangePasswordModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { 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 = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + if (!hasPassword) + { + return RedirectToPage("./SetPassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); + if (!changePasswordResult.Succeeded) + { + foreach (var error in changePasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + _logger.LogInformation("User changed their password successfully."); + StatusMessage = "Your password has been changed."; + + return RedirectToPage(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml new file mode 100644 index 00000000..c95ab92d --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml @@ -0,0 +1,33 @@ +@page +@model DeletePersonalDataModel +@{ + ViewData["Title"] = "Delete Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ + + +
+
+
+ @if (Model.RequirePassword) + { +
+ + + +
+ } + +
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs new file mode 100644 index 00000000..edf3aac3 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs @@ -0,0 +1,84 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class DeletePersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public DeletePersonalDataModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + } + + public bool RequirePassword { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + if (RequirePassword) + { + if (!await _userManager.CheckPasswordAsync(user, Input.Password)) + { + ModelState.AddModelError(string.Empty, "Incorrect password."); + return Page(); + } + } + + var result = await _userManager.DeleteAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!result.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred deleting user with ID '{userId}'."); + } + + await _signInManager.SignOutAsync(); + + _logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + return Redirect("~/"); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml new file mode 100644 index 00000000..96df7522 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml @@ -0,0 +1,25 @@ +@page +@model Disable2faModel +@{ + ViewData["Title"] = "Disable two-factor authentication (2FA)"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ + + +
+
+ +
+
diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs new file mode 100644 index 00000000..35835ecb --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class Disable2faModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public Disable2faModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!await _userManager.GetTwoFactorEnabledAsync(user)) + { + throw new InvalidOperationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); + StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; + return RedirectToPage("./TwoFactorAuthentication"); + } + } +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml new file mode 100644 index 00000000..87470c2f --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml @@ -0,0 +1,12 @@ +@page +@model DownloadPersonalDataModel +@{ + ViewData["Title"] = "Download Your Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ +@section Scripts { + +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs new file mode 100644 index 00000000..8ec9b25e --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class DownloadPersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public DownloadPersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User)); + + // Only include personal data for download + var personalData = new Dictionary(); + var personalDataProps = typeof(PhotosAppUser).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); + foreach (var p in personalDataProps) + { + personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); + } + + var logins = await _userManager.GetLoginsAsync(user); + foreach (var l in logins) + { + personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); + } + + Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json"); + return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json"); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml new file mode 100644 index 00000000..8ff8e39e --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml @@ -0,0 +1,43 @@ +@page +@model EmailModel +@{ + ViewData["Title"] = "Manage Email"; + ViewData["ActivePage"] = ManageNavPages.Email; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + @if (Model.IsEmailConfirmed) + { +
+ +
+ +
+
+ } + else + { + + + } +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs new file mode 100644 index 00000000..8d72f604 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public partial class EmailModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + + public EmailModel( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + } + + public string Username { get; set; } + + public string Email { get; set; } + + public bool IsEmailConfirmed { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "New email")] + public string NewEmail { get; set; } + } + + private async Task LoadAsync(PhotosAppUser user) + { + var email = await _userManager.GetEmailAsync(user); + Email = email; + + Input = new InputModel + { + NewEmail = email, + }; + + IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadAsync(user); + return Page(); + } + + public async Task OnPostChangeEmailAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var email = await _userManager.GetEmailAsync(user); + if (Input.NewEmail != email) + { + var userId = await _userManager.GetUserIdAsync(user); + var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmailChange", + pageHandler: null, + values: new { userId = userId, email = Input.NewEmail, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + Input.NewEmail, + "Confirm your email", + $"Please confirm your account by clicking here."); + + StatusMessage = "Confirmation link to change email sent. Please check your email."; + return RedirectToPage(); + } + + StatusMessage = "Your email is unchanged."; + return RedirectToPage(); + } + + public async Task OnPostSendVerificationEmailAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var userId = await _userManager.GetUserIdAsync(user); + var email = await _userManager.GetEmailAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = userId, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + email, + "Confirm your email", + $"Please confirm your account by clicking here."); + + StatusMessage = "Verification email sent. Please check your email."; + return RedirectToPage(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml new file mode 100644 index 00000000..6ea85104 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,53 @@ +@page +@model EnableAuthenticatorModel +@{ + ViewData["Title"] = "Configure authenticator app"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    + +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
  6. +
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs new file mode 100644 index 00000000..8c3bbde9 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -0,0 +1,157 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class EnableAuthenticatorModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly UrlEncoder _urlEncoder; + + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + public EnableAuthenticatorModel( + UserManager userManager, + ILogger logger, + UrlEncoder urlEncoder) + { + _userManager = userManager; + _logger = logger; + _urlEncoder = urlEncoder; + } + + public string SharedKey { get; set; } + + public string AuthenticatorUri { get; set; } + + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadSharedKeyAndQrCodeUriAsync(user); + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + // Strip spaces and hypens + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError("Input.Code", "Verification code is invalid."); + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + var userId = await _userManager.GetUserIdAsync(user); + _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); + + StatusMessage = "Your authenticator app has been verified."; + + if (await _userManager.CountRecoveryCodesAsync(user) == 0) + { + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes.ToArray(); + return RedirectToPage("./ShowRecoveryCodes"); + } + else + { + return RedirectToPage("./TwoFactorAuthentication"); + } + } + + private async Task LoadSharedKeyAndQrCodeUriAsync(PhotosAppUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + SharedKey = FormatKey(unformattedKey); + + var email = await _userManager.GetEmailAsync(user); + AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + AuthenticatorUriFormat, + _urlEncoder.Encode("PhotosApp"), + _urlEncoder.Encode(email), + unformattedKey); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml new file mode 100644 index 00000000..d7a3c42e --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml @@ -0,0 +1,53 @@ +@page +@model ExternalLoginsModel +@{ + ViewData["Title"] = "Manage your external logins"; + ViewData["ActivePage"] = ManageNavPages.ExternalLogins; +} + + +@if (Model.CurrentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in Model.CurrentLogins) + { + + + + + } + +
@login.ProviderDisplayName + @if (Model.ShowRemoveButton) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins?.Count > 0) +{ +

Add another service to log in.

+
+ +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs new file mode 100644 index 00000000..e164c035 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class ExternalLoginsModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ExternalLoginsModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + + public bool ShowRemoveButton { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID 'user.Id'."); + } + + CurrentLogins = await _userManager.GetLoginsAsync(user); + OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1; + return Page(); + } + + public async Task OnPostRemoveLoginAsync(string loginProvider, string providerKey) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID 'user.Id'."); + } + + var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); + if (!result.Succeeded) + { + StatusMessage = "The external login was not removed."; + return RedirectToPage(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "The external login was removed."; + return RedirectToPage(); + } + + public async Task OnPostLinkLoginAsync(string provider) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetLinkLoginCallbackAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID 'user.Id'."); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); + if (info == null) + { + throw new InvalidOperationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); + } + + var result = await _userManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + StatusMessage = "The external login was not added. External logins can only be associated with one account."; + return RedirectToPage(); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + StatusMessage = "The external login was added."; + return RedirectToPage(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 00000000..284ab59b --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -0,0 +1,27 @@ +@page +@model GenerateRecoveryCodesModel +@{ + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs new file mode 100644 index 00000000..ab7738ef --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class GenerateRecoveryCodesModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public GenerateRecoveryCodesModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' because they do not have 2FA enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!isTwoFactorEnabled) + { + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes.ToArray(); + + _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); + StatusMessage = "You have generated new recovery codes."; + return RedirectToPage("./ShowRecoveryCodes"); + } + } +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml new file mode 100644 index 00000000..2a18fdb9 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml @@ -0,0 +1,30 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Profile"; + ViewData["ActivePage"] = ManageNavPages.Index; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs new file mode 100644 index 00000000..e2a2d7a3 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public partial class IndexModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public IndexModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public string Username { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } + + private async Task LoadAsync(PhotosAppUser user) + { + var userName = await _userManager.GetUserNameAsync(user); + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); + + Username = userName; + + Input = new InputModel + { + PhoneNumber = phoneNumber + }; + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadAsync(user); + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); + if (Input.PhoneNumber != phoneNumber) + { + var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + StatusMessage = "Unexpected error when trying to set phone number."; + return RedirectToPage(); + } + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your profile has been updated"; + return RedirectToPage(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs new file mode 100644 index 00000000..311bc60a --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public static class ManageNavPages + { + public static string Index => "Index"; + + public static string Email => "Email"; + + public static string ChangePassword => "ChangePassword"; + + public static string DownloadPersonalData => "DownloadPersonalData"; + + public static string DeletePersonalData => "DeletePersonalData"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string PersonalData => "PersonalData"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); + + public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + private static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string + ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml new file mode 100644 index 00000000..d64bd826 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml @@ -0,0 +1,27 @@ +@page +@model PersonalDataModel +@{ + ViewData["Title"] = "Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ +
+
+

Your account contains personal data that you have given us. This page allows you to download or delete that data.

+

+ Deleting this data will permanently remove your account, and this cannot be recovered. +

+
+ +
+

+ Delete +

+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs new file mode 100644 index 00000000..e8337afe --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class PersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public PersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + } +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml new file mode 100644 index 00000000..081c8246 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,24 @@ +@page +@model ResetAuthenticatorModel +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs new file mode 100644 index 00000000..fce1bb2d --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class ResetAuthenticatorModel : PageModel + { + UserManager _userManager; + private readonly SignInManager _signInManager; + ILogger _logger; + + public ResetAuthenticatorModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key."; + + return RedirectToPage("./EnableAuthenticator"); + } + } +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml new file mode 100644 index 00000000..f1817aad --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml @@ -0,0 +1,35 @@ +@page +@model SetPasswordModel +@{ + ViewData["Title"] = "Set password"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

Set your password

+ +

+ You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

+
+
+
+
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs new file mode 100644 index 00000000..f75cab73 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class SetPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public SetPasswordModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + + if (hasPassword) + { + return RedirectToPage("./ChangePassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword); + if (!addPasswordResult.Succeeded) + { + foreach (var error in addPasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your password has been set."; + + return RedirectToPage(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 00000000..23fa27ba --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,25 @@ +@page +@model ShowRecoveryCodesModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs new file mode 100644 index 00000000..2f0ce7e2 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class ShowRecoveryCodesModel : PageModel + { + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public IActionResult OnGet() + { + if (RecoveryCodes == null || RecoveryCodes.Length == 0) + { + return RedirectToPage("./TwoFactorAuthentication"); + } + + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml new file mode 100644 index 00000000..a1729eba --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml @@ -0,0 +1,57 @@ +@page +@model TwoFactorAuthenticationModel +@{ + ViewData["Title"] = "Two-factor authentication (2FA)"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+@if (Model.Is2faEnabled) +{ + if (Model.RecoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (Model.RecoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (Model.RecoveryCodesLeft <= 3) + { +
+ You have @Model.RecoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + if (Model.IsMachineRemembered) + { +
+ +
+ } + Disable 2FA + Reset recovery codes +} + +
Authenticator app
+@if (!Model.HasAuthenticator) +{ + Add authenticator app +} +else +{ + Setup authenticator app + Reset authenticator app +} + +@section Scripts { + +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs new file mode 100644 index 00000000..7fff10c0 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account.Manage +{ + public class TwoFactorAuthenticationModel : PageModel + { + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}"; + + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public TwoFactorAuthenticationModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + public bool HasAuthenticator { get; set; } + + public int RecoveryCodesLeft { get; set; } + + [BindProperty] + public bool Is2faEnabled { get; set; } + + public bool IsMachineRemembered { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; + Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user); + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); + + return Page(); + } + + public async Task OnPost() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _signInManager.ForgetTwoFactorClientAsync(); + StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code."; + return RedirectToPage(); + } + } +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/_Layout.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/_Layout.cshtml new file mode 100644 index 00000000..3d882cc4 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/_Layout.cshtml @@ -0,0 +1,29 @@ +@{ + if (ViewData.TryGetValue("ParentLayout", out var parentLayout)) + { + Layout = (string)parentLayout; + } + else + { + Layout = "/Areas/Identity/Pages/_Layout.cshtml"; + } +} + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml new file mode 100644 index 00000000..4f88af2e --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -0,0 +1,15 @@ +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml new file mode 100644 index 00000000..208a4247 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml new file mode 100644 index 00000000..7755a56b --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using PhotosApp.Areas.Identity.Pages.Account.Manage diff --git a/PhotosApp/Areas/Identity/Pages/Account/Register.cshtml b/PhotosApp/Areas/Identity/Pages/Account/Register.cshtml new file mode 100644 index 00000000..96e6a9a5 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Register.cshtml @@ -0,0 +1,67 @@ +@page +@model RegisterModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+ +
+
+
+

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+
+

Use another service to register.

+
+ @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/Register.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/Register.cshtml.cs new file mode 100644 index 00000000..cc48ffac --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +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.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.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 InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public IList ExternalLogins { get; set; } + + public class InputModel + { + [Required(ErrorMessage = "Email - обязательное поле")] + [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 async Task OnGetAsync(string returnUrl = null) + { + ReturnUrl = returnUrl; + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl ??= Url.Content("~/"); + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + if (ModelState.IsValid) + { + var user = new PhotosAppUser { 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); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl }, + protocol: Request.Scheme); + + await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", + $"Please confirm your account by clicking here."); + + if (_userManager.Options.SignIn.RequireConfirmedAccount) + { + return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); + } + else + { + 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/PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml b/PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml new file mode 100644 index 00000000..c7c5d905 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml @@ -0,0 +1,22 @@ +@page +@model RegisterConfirmationModel +@{ + ViewData["Title"] = "Register confirmation"; +} + +

@ViewData["Title"]

+@{ + if (@Model.DisplayConfirmAccountLink) + { +

+ This app does not currently have a real email sender registered, see these docs for how to configure a real email sender. + Normally this would be emailed: Click here to confirm your account +

+ } + else + { +

+ Please check your email to confirm your account. +

+ } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs new file mode 100644 index 00000000..c5f01fdf --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Authorization; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class RegisterConfirmationModel : PageModel + { + private readonly UserManager _userManager; + private readonly IEmailSender _sender; + + public RegisterConfirmationModel(UserManager userManager, IEmailSender sender) + { + _userManager = userManager; + _sender = sender; + } + + public string Email { get; set; } + + public bool DisplayConfirmAccountLink { get; set; } + + public string EmailConfirmationUrl { get; set; } + + public async Task OnGetAsync(string email, string returnUrl = null) + { + if (email == null) + { + return RedirectToPage("/Index"); + } + + var user = await _userManager.FindByEmailAsync(email); + if (user == null) + { + return NotFound($"Unable to load user with email '{email}'."); + } + + Email = email; + // Once you add a real email sender, you should remove this code that lets you confirm the account + DisplayConfirmAccountLink = true; + if (DisplayConfirmAccountLink) + { + var userId = await _userManager.GetUserIdAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + EmailConfirmationUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl }, + protocol: Request.Scheme); + } + + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml new file mode 100644 index 00000000..16d11e9d --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml @@ -0,0 +1,26 @@ +@page +@model ResendEmailConfirmationModel +@{ + ViewData["Title"] = "Resend email confirmation"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs new file mode 100644 index 00000000..c7f84d17 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs @@ -0,0 +1,74 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +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.AspNetCore.WebUtilities; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResendEmailConfirmationModel : PageModel + { + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + + public ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender) + { + _userManager = userManager; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required(ErrorMessage = "Email - обязательное поле")] + [EmailAddress] + public string Email { get; set; } + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null) + { + ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email."); + return Page(); + } + + var userId = await _userManager.GetUserIdAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { userId = userId, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + Input.Email, + "Confirm your email", + $"Please confirm your account by clicking here."); + + ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email."); + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml new file mode 100644 index 00000000..27bc951b --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml @@ -0,0 +1,37 @@ +@page +@model ResetPasswordModel +@{ + ViewData["Title"] = "Reset password"; +} + +

@ViewData["Title"]

+

Reset your password.

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs new file mode 100644 index 00000000..347f7f29 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordModel : PageModel + { + private readonly UserManager _userManager; + + public ResetPasswordModel(UserManager userManager) + { + _userManager = userManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + 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)] + 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 string Code { get; set; } + } + + public IActionResult OnGet(string code = null) + { + if (code == null) + { + return BadRequest("A code must be supplied for password reset."); + } + else + { + Input = new InputModel + { + Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)) + }; + return Page(); + } + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToPage("./ResetPasswordConfirmation"); + } + + var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + return RedirectToPage("./ResetPasswordConfirmation"); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml b/PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 00000000..c52552f3 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,10 @@ +@page +@model ResetPasswordConfirmationModel +@{ + ViewData["Title"] = "Reset password confirmation"; +} + +

@ViewData["Title"]

+

+ Your password has been reset. Please click here to log in. +

diff --git a/PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs new file mode 100644 index 00000000..06c05a3c --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PhotosApp.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordConfirmationModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/_StatusMessage.cshtml b/PhotosApp/Areas/Identity/Pages/Account/_StatusMessage.cshtml new file mode 100644 index 00000000..e9968413 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/PhotosApp/Areas/Identity/Pages/Account/_ViewImports.cshtml b/PhotosApp/Areas/Identity/Pages/Account/_ViewImports.cshtml new file mode 100644 index 00000000..939a5c8e --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using PhotosApp.Areas.Identity.Pages.Account \ No newline at end of file diff --git a/PhotosApp/Areas/Identity/Pages/Error.cshtml b/PhotosApp/Areas/Identity/Pages/Error.cshtml new file mode 100644 index 00000000..b1f3143a --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Error.cshtml @@ -0,0 +1,23 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/PhotosApp/Areas/Identity/Pages/Error.cshtml.cs b/PhotosApp/Areas/Identity/Pages/Error.cshtml.cs new file mode 100644 index 00000000..1c2f9a7f --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/Error.cshtml.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PhotosApp.Areas.Identity.Pages +{ + [AllowAnonymous] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} diff --git a/PhotosApp/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/PhotosApp/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 00000000..9e26f3b8 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/PhotosApp/Areas/Identity/Pages/_ViewImports.cshtml b/PhotosApp/Areas/Identity/Pages/_ViewImports.cshtml new file mode 100644 index 00000000..836212d9 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Identity +@using PhotosApp.Areas.Identity +@using PhotosApp.Areas.Identity.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@using PhotosApp.Areas.Identity.Data diff --git a/PhotosApp/Areas/Identity/Pages/_ViewStart.cshtml b/PhotosApp/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 00000000..a4712a43 --- /dev/null +++ b/PhotosApp/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,4 @@ + +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} diff --git a/PhotosApp/Controllers/PhotosController.cs b/PhotosApp/Controllers/PhotosController.cs index fbeb9840..d02ed34d 100644 --- a/PhotosApp/Controllers/PhotosController.cs +++ b/PhotosApp/Controllers/PhotosController.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using AutoMapper; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using PhotosApp.Data; @@ -11,6 +13,7 @@ namespace PhotosApp.Controllers { + [Authorize] public class PhotosController : Controller { private readonly IPhotosRepository photosRepository; @@ -22,6 +25,7 @@ public PhotosController(IPhotosRepository photosRepository, IMapper mapper) this.mapper = mapper; } + [AllowAnonymous] public async Task Index() { var ownerId = GetOwnerId(); @@ -137,7 +141,7 @@ public async Task DeletePhoto(Guid id) private string GetOwnerId() { - return "a83b72ed-3f99-44b5-aa32-f9d03e7eb1fd"; + return User.FindFirstValue(ClaimTypes.NameIdentifier); } } } diff --git a/PhotosApp/Data/PhotosAppDataExtensions.cs b/PhotosApp/Data/PhotosAppDataExtensions.cs index b4614cd8..579f0eab 100644 --- a/PhotosApp/Data/PhotosAppDataExtensions.cs +++ b/PhotosApp/Data/PhotosAppDataExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using PhotosApp.Areas.Identity.Data; using PhotosApp.Services.TicketStores; namespace PhotosApp.Data @@ -23,9 +24,13 @@ public static void PrepareData(this IHost host) if (env.IsDevelopment()) { scope.ServiceProvider.GetRequiredService().Database.Migrate(); + scope.ServiceProvider.GetRequiredService().Database.Migrate(); var photosDbContext = scope.ServiceProvider.GetRequiredService(); photosDbContext.SeedWithSamplePhotosAsync().Wait(); + + var usersDbContext = scope.ServiceProvider.GetRequiredService>(); + usersDbContext.SeedWithSampleUsersAsync().Wait(); } } catch (Exception e) diff --git a/PhotosApp/Migrations/UsersDb/20231109075354_Users.Designer.cs b/PhotosApp/Migrations/UsersDb/20231109075354_Users.Designer.cs new file mode 100644 index 00000000..34b354fe --- /dev/null +++ b/PhotosApp/Migrations/UsersDb/20231109075354_Users.Designer.cs @@ -0,0 +1,270 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Migrations.UsersDb +{ + [DbContext(typeof(UsersDbContext))] + [Migration("20231109075354_Users")] + partial class Users + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("PhotosApp.Areas.Identity.Data.PhotosAppUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + 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("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PhotosApp.Areas.Identity.Data.PhotosAppUser", 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("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhotosApp/Migrations/UsersDb/20231109075354_Users.cs b/PhotosApp/Migrations/UsersDb/20231109075354_Users.cs new file mode 100644 index 00000000..0597a9b5 --- /dev/null +++ b/PhotosApp/Migrations/UsersDb/20231109075354_Users.cs @@ -0,0 +1,217 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PhotosApp.Migrations.UsersDb +{ + public partial class Users : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ProviderKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "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/PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs b/PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs new file mode 100644 index 00000000..bb29b53e --- /dev/null +++ b/PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs @@ -0,0 +1,268 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Migrations.UsersDb +{ + [DbContext(typeof(UsersDbContext))] + partial class UsersDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("PhotosApp.Areas.Identity.Data.PhotosAppUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + 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("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PhotosApp.Areas.Identity.Data.PhotosAppUser", 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("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhotosApp/ScaffoldingReadMe.txt b/PhotosApp/ScaffoldingReadMe.txt new file mode 100644 index 00000000..6e6208dc --- /dev/null +++ b/PhotosApp/ScaffoldingReadMe.txt @@ -0,0 +1,3 @@ +Support for ASP.NET Core Identity was added to your project. + +For setup and configuration information, see https://go.microsoft.com/fwlink/?linkid=2116645. diff --git a/PhotosApp/Services/SimplePasswordHasher.cs b/PhotosApp/Services/SimplePasswordHasher.cs index e3dae0c7..6bf31d9f 100644 --- a/PhotosApp/Services/SimplePasswordHasher.cs +++ b/PhotosApp/Services/SimplePasswordHasher.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Security.Cryptography; +using System.Text; using Microsoft.AspNetCore.Cryptography.KeyDerivation; using Microsoft.AspNetCore.Identity; using NUnit.Framework; @@ -25,10 +26,14 @@ public string HashPassword(TUser user, string password) public PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword) { - byte[] expectedHashBytes = null; - byte[] actualHashBytes = null; + byte[] actualHashBytes = Convert.FromBase64String(hashedPassword); - throw new NotImplementedException(); + var salt = new byte[SaltSizeInBits/8]; + Array.Copy(actualHashBytes, salt, SaltSizeInBits / 8); + + byte[] hashBytes = GetHashBytes(providedPassword, salt); + byte[] expectedHashBytes = ConcatenateBytes(salt, hashBytes); + // Если providedPassword корректен, то в результате хэширования его с той же самой солью, // что и оригинальный пароль, должен получаться тот же самый хэш. diff --git a/PhotosApp/Startup.cs b/PhotosApp/Startup.cs index 2a62000e..4dd19dc6 100644 --- a/PhotosApp/Startup.cs +++ b/PhotosApp/Startup.cs @@ -2,14 +2,17 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using PhotosApp.Areas.Identity.Data; using PhotosApp.Clients; using PhotosApp.Clients.Models; using PhotosApp.Data; using PhotosApp.Models; +using PhotosApp.Services; using Serilog; namespace PhotosApp @@ -31,9 +34,10 @@ public void ConfigureServices(IServiceCollection services) services.Configure(configuration.GetSection("PhotosService")); var mvc = services.AddControllersWithViews(); + services.AddRazorPages(); if (env.IsDevelopment()) mvc.AddRazorRuntimeCompilation(); - + // NOTE: Подключение IHttpContextAccessor, чтобы можно было получать HttpContext там, // где это не получается сделать более явно. services.AddHttpContextAccessor(); @@ -59,6 +63,8 @@ public void ConfigureServices(IServiceCollection services) }, new System.Reflection.Assembly[0]); services.AddTransient(); + + services.AddScoped, SimplePasswordHasher>(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -77,9 +83,14 @@ public void Configure(IApplicationBuilder app) app.UseSerilogRequestLogging(); app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("default", "{controller=Photos}/{action=Index}/{id?}"); + endpoints.MapRazorPages(); }); } } diff --git a/PhotosApp/Views/Shared/_Layout.cshtml b/PhotosApp/Views/Shared/_Layout.cshtml index 8fc11d3d..298cb7b4 100644 --- a/PhotosApp/Views/Shared/_Layout.cshtml +++ b/PhotosApp/Views/Shared/_Layout.cshtml @@ -39,7 +39,12 @@ + + +
diff --git a/PhotosApp/Views/Shared/_LoginPartial.cshtml b/PhotosApp/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 00000000..949ee6a5 --- /dev/null +++ b/PhotosApp/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,28 @@ +@using Microsoft.AspNetCore.Identity +@using PhotosApp.Areas.Identity.Data + +@inject SignInManager SignInManager +@inject UserManager UserManager + + diff --git a/PhotosApp/appsettings.json b/PhotosApp/appsettings.json index 6907e11b..cd65e99c 100644 --- a/PhotosApp/appsettings.json +++ b/PhotosApp/appsettings.json @@ -6,7 +6,8 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "PhotosDbContextConnection": "Data Source=PhotosApp.db" + "PhotosDbContextConnection": "Data Source=PhotosApp.db", + "UsersDbContextConnection": "Data Source=PhotosApp.db" }, "SimpleEmailSender": { "Host": "smtp.gmail.com", @@ -18,4 +19,4 @@ "PhotosService": { "ServiceUrl": "https://localhost:6001" } -} +} \ No newline at end of file From 4ed55146cb17d40d1fb6ee08a5289c59454bda3d Mon Sep 17 00:00:00 2001 From: Ilya Lagunov Date: Thu, 16 Nov 2023 13:09:45 +0500 Subject: [PATCH 2/7] Task 2 done --- .../Areas/Identity/IdentityHostingStartup.cs | 28 ++++++++++- PhotosApp/Data/PhotosAppDataExtensions.cs | 4 ++ .../20231116080527_Tickets.Designer.cs | 46 +++++++++++++++++++ .../TicketsDb/20231116080527_Tickets.cs | 32 +++++++++++++ .../TicketsDbContextModelSnapshot.cs | 44 ++++++++++++++++++ PhotosApp/appsettings.json | 3 +- 6 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.Designer.cs create mode 100644 PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.cs create mode 100644 PhotosApp/Migrations/TicketsDb/TicketsDbContextModelSnapshot.cs diff --git a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs index 65cf4d78..34ed2a9f 100644 --- a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs +++ b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI; @@ -7,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using PhotosApp.Areas.Identity.Data; using PhotosApp.Services; +using PhotosApp.Services.TicketStores; [assembly: HostingStartup(typeof(PhotosApp.Areas.Identity.IdentityHostingStartup))] namespace PhotosApp.Areas.Identity @@ -15,15 +17,22 @@ public class IdentityHostingStartup : IHostingStartup { public void Configure(IWebHostBuilder builder) { - builder.ConfigureServices((context, services) => { + builder.ConfigureServices((context, services) => + { services.AddDbContext(options => options.UseSqlite( context.Configuration.GetConnectionString("UsersDbContextConnection"))); + + services.AddDbContext(options => + options.UseSqlite( + context.Configuration.GetConnectionString("TicketsDbContextConnection"))); + services.AddDefaultIdentity() .AddPasswordValidator>() .AddErrorDescriber() - .AddEntityFrameworkStores(); + .AddEntityFrameworkStores() + .AddEntityFrameworkStores(); services.Configure(options => { @@ -43,6 +52,21 @@ public void Configure(IWebHostBuilder builder) options.IterationCount = 12000; }); + services.AddTransient(); + services.ConfigureApplicationCookie(options => + { + var serviceProvider = services.BuildServiceProvider(); + options.SessionStore = serviceProvider.GetRequiredService(); + options.AccessDeniedPath = "/Identity/Account/AccessDenied"; + options.Cookie.Name = "PhotosApp.Auth"; + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromMinutes(60); + options.LoginPath = "/Identity/Account/Login"; + // ReturnUrlParameter requires + //using Microsoft.AspNetCore.Authentication.Cookies; + options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; + options.SlidingExpiration = true; + }); }); } } diff --git a/PhotosApp/Data/PhotosAppDataExtensions.cs b/PhotosApp/Data/PhotosAppDataExtensions.cs index 579f0eab..99128919 100644 --- a/PhotosApp/Data/PhotosAppDataExtensions.cs +++ b/PhotosApp/Data/PhotosAppDataExtensions.cs @@ -25,12 +25,16 @@ public static void PrepareData(this IHost host) { scope.ServiceProvider.GetRequiredService().Database.Migrate(); scope.ServiceProvider.GetRequiredService().Database.Migrate(); + scope.ServiceProvider.GetRequiredService().Database.Migrate(); var photosDbContext = scope.ServiceProvider.GetRequiredService(); photosDbContext.SeedWithSamplePhotosAsync().Wait(); var usersDbContext = scope.ServiceProvider.GetRequiredService>(); usersDbContext.SeedWithSampleUsersAsync().Wait(); + + var ticketsDbContext = scope.ServiceProvider.GetRequiredService(); + ticketsDbContext.SeedWithSampleTicketsAsync().Wait(); } } catch (Exception e) diff --git a/PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.Designer.cs b/PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.Designer.cs new file mode 100644 index 00000000..892ff510 --- /dev/null +++ b/PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.Designer.cs @@ -0,0 +1,46 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhotosApp.Services.TicketStores; + +namespace PhotosApp.Migrations.TicketsDb +{ + [DbContext(typeof(TicketsDbContext))] + [Migration("20231116080527_Tickets")] + partial class Tickets + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("PhotosApp.Services.TicketStores.TicketEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("LastActivity") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.cs b/PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.cs new file mode 100644 index 00000000..dcf5ecba --- /dev/null +++ b/PhotosApp/Migrations/TicketsDb/20231116080527_Tickets.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PhotosApp.Migrations.TicketsDb +{ + public partial class Tickets : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "BLOB", nullable: true), + LastActivity = table.Column(type: "TEXT", nullable: true), + Expires = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tickets"); + } + } +} diff --git a/PhotosApp/Migrations/TicketsDb/TicketsDbContextModelSnapshot.cs b/PhotosApp/Migrations/TicketsDb/TicketsDbContextModelSnapshot.cs new file mode 100644 index 00000000..364b98e3 --- /dev/null +++ b/PhotosApp/Migrations/TicketsDb/TicketsDbContextModelSnapshot.cs @@ -0,0 +1,44 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhotosApp.Services.TicketStores; + +namespace PhotosApp.Migrations.TicketsDb +{ + [DbContext(typeof(TicketsDbContext))] + partial class TicketsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("PhotosApp.Services.TicketStores.TicketEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("LastActivity") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhotosApp/appsettings.json b/PhotosApp/appsettings.json index cd65e99c..ea05c3d8 100644 --- a/PhotosApp/appsettings.json +++ b/PhotosApp/appsettings.json @@ -7,7 +7,8 @@ "AllowedHosts": "*", "ConnectionStrings": { "PhotosDbContextConnection": "Data Source=PhotosApp.db", - "UsersDbContextConnection": "Data Source=PhotosApp.db" + "UsersDbContextConnection": "Data Source=PhotosApp.db", + "TicketsDbContextConnection": "Data Source=PhotosApp.db" }, "SimpleEmailSender": { "Host": "smtp.gmail.com", From 39fc2a62006f7ffeb3452f1ebb9c892261673e1d Mon Sep 17 00:00:00 2001 From: Vadim <92179377+Zhuzhalica@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:50:17 +0500 Subject: [PATCH 3/7] task 3 almost done --- .../Areas/Identity/Data/PhotosAppUser.cs | 1 + .../Areas/Identity/IdentityHostingStartup.cs | 28 ++ PhotosApp/Controllers/DevController.cs | 4 +- PhotosApp/Controllers/PhotosController.cs | 11 +- PhotosApp/Data/PhotosAppDataExtensions.cs | 18 +- .../UsersDb/20231116091021_Paid.Designer.cs | 273 ++++++++++++++++++ .../Migrations/UsersDb/20231116091021_Paid.cs | 24 ++ .../UsersDb/UsersDbContextModelSnapshot.cs | 3 + .../CustomClaimsPrincipalFactory.cs | 18 +- .../Authorization/MustOwnPhotoHandler.cs | 15 +- PhotosApp/Startup.cs | 3 + PhotosApp/Views/Photos/GetPhoto.cshtml | 12 +- PhotosApp/Views/Photos/Index.cshtml | 6 +- PhotosApp/Views/Shared/_Layout.cshtml | 127 ++++---- 14 files changed, 464 insertions(+), 79 deletions(-) create mode 100644 PhotosApp/Migrations/UsersDb/20231116091021_Paid.Designer.cs create mode 100644 PhotosApp/Migrations/UsersDb/20231116091021_Paid.cs diff --git a/PhotosApp/Areas/Identity/Data/PhotosAppUser.cs b/PhotosApp/Areas/Identity/Data/PhotosAppUser.cs index 730cd6fd..35673ce8 100644 --- a/PhotosApp/Areas/Identity/Data/PhotosAppUser.cs +++ b/PhotosApp/Areas/Identity/Data/PhotosAppUser.cs @@ -9,5 +9,6 @@ namespace PhotosApp.Areas.Identity.Data // Add profile data for application users by adding properties to the PhotosAppUser class public class PhotosAppUser : IdentityUser { + public bool Paid { get; set; } } } diff --git a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs index 34ed2a9f..8a7a0664 100644 --- a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs +++ b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using PhotosApp.Areas.Identity.Data; using PhotosApp.Services; +using PhotosApp.Services.Authorization; using PhotosApp.Services.TicketStores; [assembly: HostingStartup(typeof(PhotosApp.Areas.Identity.IdentityHostingStartup))] @@ -29,6 +30,8 @@ public void Configure(IWebHostBuilder builder) services.AddDefaultIdentity() + .AddRoles() + .AddClaimsPrincipalFactory() .AddPasswordValidator>() .AddErrorDescriber() .AddEntityFrameworkStores() @@ -67,6 +70,31 @@ public void Configure(IWebHostBuilder builder) options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; options.SlidingExpiration = true; }); + + services.AddAuthorization(options => + { + options.AddPolicy( + "Beta", + policyBuilder => + { + policyBuilder.RequireAuthenticatedUser(); + policyBuilder.RequireClaim("testing", "beta"); + }); + options.AddPolicy( + "CanAddPhoto", + policyBuilder => + { + policyBuilder.RequireAuthenticatedUser(); + policyBuilder.RequireClaim("subscription", "paid"); + }); + options.AddPolicy( + "MustOwnPhoto", + policyBuilder => + { + policyBuilder.RequireAuthenticatedUser(); + policyBuilder.AddRequirements(new MustOwnPhotoRequirement()); + }); + }); }); } } diff --git a/PhotosApp/Controllers/DevController.cs b/PhotosApp/Controllers/DevController.cs index cde0fb4c..f2c0e7f6 100644 --- a/PhotosApp/Controllers/DevController.cs +++ b/PhotosApp/Controllers/DevController.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; namespace PhotosApp.Controllers { + [Authorize(Roles = "Dev")] public class DevController : Controller { public IActionResult Decode() diff --git a/PhotosApp/Controllers/PhotosController.cs b/PhotosApp/Controllers/PhotosController.cs index d02ed34d..33a8d8dd 100644 --- a/PhotosApp/Controllers/PhotosController.cs +++ b/PhotosApp/Controllers/PhotosController.cs @@ -35,7 +35,8 @@ public async Task Index() var model = new PhotoIndexModel(photos.ToList()); return View(model); } - + + [Authorize(Policy = "MustOwnPhoto")] public async Task GetPhoto(Guid id) { var photoEntity = await photosRepository.GetPhotoMetaAsync(id); @@ -49,6 +50,7 @@ public async Task GetPhoto(Guid id) } [HttpGet("photos/{id}")] + [Authorize(Policy = "MustOwnPhoto")] public async Task GetPhotoFile(Guid id) { var photoContent = await photosRepository.GetPhotoContentAsync(id); @@ -65,6 +67,8 @@ public IActionResult AddPhoto() [HttpPost] [ValidateAntiForgeryToken] + [Authorize(Policy = "CanAddPhoto")] + [Authorize(Policy = "MustOwnPhoto")] public async Task AddPhoto(AddPhotoModel addPhotoModel) { if (addPhotoModel == null || !ModelState.IsValid) @@ -93,6 +97,8 @@ public async Task AddPhoto(AddPhotoModel addPhotoModel) return RedirectToAction("Index"); } + [Authorize(Policy = "Beta")] + [Authorize(Policy = "MustOwnPhoto")] public async Task EditPhoto(Guid id) { var photo = await photosRepository.GetPhotoMetaAsync(id); @@ -109,6 +115,8 @@ public async Task EditPhoto(Guid id) [HttpPost] [ValidateAntiForgeryToken] + [Authorize(Policy = "Beta")] + [Authorize(Policy = "MustOwnPhoto")] public async Task EditPhoto(EditPhotoModel editPhotoModel) { if (editPhotoModel == null || !ModelState.IsValid) @@ -127,6 +135,7 @@ public async Task EditPhoto(EditPhotoModel editPhotoModel) } [HttpPost] + [Authorize(Policy = "MustOwnPhoto")] public async Task DeletePhoto(Guid id) { var photoEntity = await photosRepository.GetPhotoMetaAsync(id); diff --git a/PhotosApp/Data/PhotosAppDataExtensions.cs b/PhotosApp/Data/PhotosAppDataExtensions.cs index 99128919..d57f713b 100644 --- a/PhotosApp/Data/PhotosAppDataExtensions.cs +++ b/PhotosApp/Data/PhotosAppDataExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; @@ -29,6 +30,9 @@ public static void PrepareData(this IHost host) var photosDbContext = scope.ServiceProvider.GetRequiredService(); photosDbContext.SeedWithSamplePhotosAsync().Wait(); + + var roleManager = scope.ServiceProvider.GetRequiredService>(); + roleManager.SeedWithSampleRolesAsync().Wait(); var usersDbContext = scope.ServiceProvider.GetRequiredService>(); usersDbContext.SeedWithSampleUsersAsync().Wait(); @@ -149,41 +153,43 @@ private static async Task SeedWithSampleTicketsAsync(this TicketsDbContext dbCon await dbContext.SaveChangesAsync(); } - private static async Task SeedWithSampleUsersAsync(this UserManager userManager) - where TUser : IdentityUser, new() + private static async Task SeedWithSampleUsersAsync(this UserManager userManager) { // NOTE: ToList важен, так как при удалении пользователя меняется список пользователей foreach (var user in userManager.Users.ToList()) await userManager.DeleteAsync(user); { - var user = new TUser + var user = new PhotosAppUser { Id = "a83b72ed-3f99-44b5-aa32-f9d03e7eb1fd", UserName = "vicky@gmail.com", Email = "vicky@gmail.com" }; await userManager.RegisterUserIfNotExists(user, "Pass!2"); + await userManager.AddClaimAsync(user, new Claim("testing", "beta")); } { - var user = new TUser + var user = new PhotosAppUser { Id = "dcaec9ce-91c9-4105-8d4d-eee3365acd82", UserName = "cristina@gmail.com", - Email = "cristina@gmail.com" + Email = "cristina@gmail.com", + Paid = true }; await userManager.RegisterUserIfNotExists(user, "Pass!2"); } { - var user = new TUser + var user = new PhotosAppUser { Id = "b9991f69-b4c1-477d-9432-2f7cf6099e02", UserName = "dev@gmail.com", Email = "dev@gmail.com" }; await userManager.RegisterUserIfNotExists(user, "Pass!2"); + await userManager.AddToRoleAsync(user, "Dev"); } } diff --git a/PhotosApp/Migrations/UsersDb/20231116091021_Paid.Designer.cs b/PhotosApp/Migrations/UsersDb/20231116091021_Paid.Designer.cs new file mode 100644 index 00000000..1feceb21 --- /dev/null +++ b/PhotosApp/Migrations/UsersDb/20231116091021_Paid.Designer.cs @@ -0,0 +1,273 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhotosApp.Areas.Identity.Data; + +namespace PhotosApp.Migrations.UsersDb +{ + [DbContext(typeof(UsersDbContext))] + [Migration("20231116091021_Paid")] + partial class Paid + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("PhotosApp.Areas.Identity.Data.PhotosAppUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Paid") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + 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("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PhotosApp.Areas.Identity.Data.PhotosAppUser", 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("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PhotosApp.Areas.Identity.Data.PhotosAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhotosApp/Migrations/UsersDb/20231116091021_Paid.cs b/PhotosApp/Migrations/UsersDb/20231116091021_Paid.cs new file mode 100644 index 00000000..33c64201 --- /dev/null +++ b/PhotosApp/Migrations/UsersDb/20231116091021_Paid.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PhotosApp.Migrations.UsersDb +{ + public partial class Paid : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Paid", + table: "AspNetUsers", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Paid", + table: "AspNetUsers"); + } + } +} diff --git a/PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs b/PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs index bb29b53e..36d55265 100644 --- a/PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs +++ b/PhotosApp/Migrations/UsersDb/UsersDbContextModelSnapshot.cs @@ -181,6 +181,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("TEXT"); + b.Property("Paid") + .HasColumnType("INTEGER"); + b.Property("PasswordHash") .HasColumnType("TEXT"); diff --git a/PhotosApp/Services/Authorization/CustomClaimsPrincipalFactory.cs b/PhotosApp/Services/Authorization/CustomClaimsPrincipalFactory.cs index fae9bbf5..da8f4905 100644 --- a/PhotosApp/Services/Authorization/CustomClaimsPrincipalFactory.cs +++ b/PhotosApp/Services/Authorization/CustomClaimsPrincipalFactory.cs @@ -3,19 +3,21 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using PhotosApp.Areas.Identity.Data; namespace PhotosApp.Services.Authorization { - public class CustomClaimsPrincipalFactory : UserClaimsPrincipalFactory + public class CustomClaimsPrincipalFactory : UserClaimsPrincipalFactory { public CustomClaimsPrincipalFactory( - UserManager userManager, + UserManager userManager, RoleManager roleManager, IOptions optionsAccessor) : base(userManager, roleManager, optionsAccessor) - { } + { + } - public override async Task CreateAsync(IdentityUser user) + public override async Task CreateAsync(PhotosAppUser user) { var principal = await base.CreateAsync(user); var claimsIdentity = (ClaimsIdentity)principal.Identity; @@ -26,7 +28,13 @@ public override async Task CreateAsync(IdentityUser user) // new Claim("type", "value") // }); - throw new NotImplementedException(); + if (user.Paid) + { + claimsIdentity.AddClaims(new[] + { + new Claim("subscription", "paid") + }); + } return principal; } diff --git a/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs b/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs index d242cec8..173fdfeb 100644 --- a/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs +++ b/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs @@ -28,7 +28,20 @@ protected override async Task HandleRequirementAsync( // NOTE: RouteData содержит информацию о пути и параметрах запроса. // Ее сформировал UseRouting и к моменту авторизации уже отработал. var routeData = httpContext?.GetRouteData(); + var hasId = routeData.Values.TryGetValue("id", out var id); + if (!hasId) + return; + + var photo = await photosRepository.GetPhotoMetaAsync((string) id); + if (photo.OwnerId == userId) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } // NOTE: Использовать, если нужное условие выполняется // context.Succeed(requirement); @@ -37,8 +50,6 @@ protected override async Task HandleRequirementAsync( // NOTE: Этот метод получает информацию о фотографии, в том числе о владельце // await photosRepository.GetPhotoMetaAsync(...) - - throw new NotImplementedException(); } } } \ No newline at end of file diff --git a/PhotosApp/Startup.cs b/PhotosApp/Startup.cs index 4dd19dc6..52a6a22e 100644 --- a/PhotosApp/Startup.cs +++ b/PhotosApp/Startup.cs @@ -1,5 +1,6 @@ using AutoMapper; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; @@ -13,6 +14,7 @@ using PhotosApp.Data; using PhotosApp.Models; using PhotosApp.Services; +using PhotosApp.Services.Authorization; using Serilog; namespace PhotosApp @@ -50,6 +52,7 @@ public void ConfigureServices(IServiceCollection services) // o.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=PhotosApp;Trusted_Connection=True;")); services.AddScoped(); + services.AddScoped(); services.AddAutoMapper(cfg => { diff --git a/PhotosApp/Views/Photos/GetPhoto.cshtml b/PhotosApp/Views/Photos/GetPhoto.cshtml index 3c125c0e..4b8eec68 100644 --- a/PhotosApp/Views/Photos/GetPhoto.cshtml +++ b/PhotosApp/Views/Photos/GetPhoto.cshtml @@ -1,4 +1,7 @@ -@model GetPhotoModel +@using Microsoft.AspNetCore.Authorization +@inject IAuthorizationService AuthorizationService + +@model GetPhotoModel @{ ViewData["Title"] = "Фото"; } @@ -10,10 +13,13 @@
@Model.Photo.Title
- Изменить подпись + @if ((await AuthorizationService.AuthorizeAsync(User, "Beta")).Succeeded) + { + Изменить подпись + }  
+ asp-controller="Photos" asp-action="DeletePhoto" asp-route-id="@Model.Photo.Id">
diff --git a/PhotosApp/Views/Photos/Index.cshtml b/PhotosApp/Views/Photos/Index.cshtml index e3fa8018..273b7ee7 100644 --- a/PhotosApp/Views/Photos/Index.cshtml +++ b/PhotosApp/Views/Photos/Index.cshtml @@ -1,4 +1,6 @@ -@model PhotoIndexModel +@using Microsoft.AspNetCore.Authorization +@inject IAuthorizationService AuthorizationService +@model PhotoIndexModel @{ ViewData["Title"] = "Все фото"; } @@ -25,7 +27,7 @@ else {
- @if (User.Identity.IsAuthenticated) + @if (User.Identity.IsAuthenticated && (await AuthorizationService.AuthorizeAsync(User, "PolicyName")).Succeeded) { Ничего не найдено...  Загрузите  свои фото diff --git a/PhotosApp/Views/Shared/_Layout.cshtml b/PhotosApp/Views/Shared/_Layout.cshtml index 298cb7b4..5a591365 100644 --- a/PhotosApp/Views/Shared/_Layout.cshtml +++ b/PhotosApp/Views/Shared/_Layout.cshtml @@ -1,12 +1,15 @@ - +@using Microsoft.AspNetCore.Authorization +@inject IAuthorizationService AuthorizationService + + - - + + @(ViewData["Title"] != null ? $"{ViewData["Title"]} - Web Photos" : "Web Photos") - + - + -
- +
+ + + + + + +
+
+ @RenderBody() +
+
+ - - +
-
- @RenderBody() -
+ © Web Photos
- +
-
-
- © Web Photos -
-
- - - - - - - - - + + + + + + - - - + + - - + + - @RenderSection("Scripts", required: false) +@RenderSection("Scripts", required: false) - + \ No newline at end of file From bb977bec3679af9d9136e652042020f747bf1f3e Mon Sep 17 00:00:00 2001 From: Vadim <92179377+Zhuzhalica@users.noreply.github.com> Date: Thu, 23 Nov 2023 10:07:13 +0500 Subject: [PATCH 4/7] task 3 done --- PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs b/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs index 173fdfeb..44727fab 100644 --- a/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs +++ b/PhotosApp/Services/Authorization/MustOwnPhotoHandler.cs @@ -32,7 +32,7 @@ protected override async Task HandleRequirementAsync( if (!hasId) return; - var photo = await photosRepository.GetPhotoMetaAsync((string) id); + var photo = await photosRepository.GetPhotoMetaAsync(new Guid(id.ToString())); if (photo.OwnerId == userId) { From 3ff09be3409ab29ea1145e62f2382a26316c629e Mon Sep 17 00:00:00 2001 From: Vadim <92179377+Zhuzhalica@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:25:46 +0500 Subject: [PATCH 5/7] task 4 done --- PhotosApp/Areas/Identity/IdentityHostingStartup.cs | 7 +++++++ PhotosApp/appsettings.json | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs index 8a7a0664..788a9a57 100644 --- a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs +++ b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs @@ -71,6 +71,13 @@ public void Configure(IWebHostBuilder builder) options.SlidingExpiration = true; }); + services.AddAuthentication() + .AddGoogle("Google", options => + { + options.ClientId = context.Configuration["Authentication:Google:ClientId"]; + options.ClientSecret = context.Configuration["Authentication:Google:ClientSecret"]; + }); + services.AddAuthorization(options => { options.AddPolicy( diff --git a/PhotosApp/appsettings.json b/PhotosApp/appsettings.json index ea05c3d8..9b5b9db2 100644 --- a/PhotosApp/appsettings.json +++ b/PhotosApp/appsettings.json @@ -19,5 +19,11 @@ }, "PhotosService": { "ServiceUrl": "https://localhost:6001" + }, + "Authentication": { + "Google": { + "ClientId": "265864215463-807n2gujifbaqdmr9ha90u5g2ikkvb0i.apps.googleusercontent.com", + "ClientSecret": "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" + } } } \ No newline at end of file From 3ac3828ba129d88ffb22b4f77449ad6151329f45 Mon Sep 17 00:00:00 2001 From: Ilya Lagunov Date: Thu, 23 Nov 2023 12:42:30 +0500 Subject: [PATCH 6/7] Task 5 Done --- PhotosApp/Areas/Identity/IdentityHostingStartup.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs index 788a9a57..5960f69b 100644 --- a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs +++ b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs @@ -102,6 +102,17 @@ public void Configure(IWebHostBuilder builder) policyBuilder.AddRequirements(new MustOwnPhotoRequirement()); }); }); + + services.AddTransient(serviceProvider => + new SimpleEmailSender( + serviceProvider.GetRequiredService>(), + serviceProvider.GetRequiredService(), + context.Configuration["SimpleEmailSender:Host"], + context.Configuration.GetValue("SimpleEmailSender:Port"), + context.Configuration.GetValue("SimpleEmailSender:EnableSSL"), + context.Configuration["SimpleEmailSender:UserName"], + context.Configuration["SimpleEmailSender:Password"] + )); }); } } From e113bc6bc6a43e4a122ae34adbb5c927cede2a29 Mon Sep 17 00:00:00 2001 From: German Markov Date: Thu, 23 Nov 2023 14:31:40 +0500 Subject: [PATCH 7/7] Task 6 done --- .../Areas/Identity/IdentityHostingStartup.cs | 63 ++++++++++++++++--- PhotosApp/Controllers/DevController.cs | 3 +- PhotosApp/Services/TemporaryTokens.cs | 9 ++- PhotosApp/Views/Shared/_Layout.cshtml | 3 +- PhotosApp/appsettings.json | 2 +- 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs index 5960f69b..0190cb65 100644 --- a/PhotosApp/Areas/Identity/IdentityHostingStartup.cs +++ b/PhotosApp/Areas/Identity/IdentityHostingStartup.cs @@ -1,17 +1,25 @@ using System; +using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI; +using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; using PhotosApp.Areas.Identity.Data; using PhotosApp.Services; using PhotosApp.Services.Authorization; using PhotosApp.Services.TicketStores; [assembly: HostingStartup(typeof(PhotosApp.Areas.Identity.IdentityHostingStartup))] + namespace PhotosApp.Areas.Identity { public class IdentityHostingStartup : IHostingStartup @@ -23,11 +31,11 @@ public void Configure(IWebHostBuilder builder) services.AddDbContext(options => options.UseSqlite( context.Configuration.GetConnectionString("UsersDbContextConnection"))); - + services.AddDbContext(options => options.UseSqlite( context.Configuration.GetConnectionString("TicketsDbContextConnection"))); - + services.AddDefaultIdentity() .AddRoles() @@ -36,7 +44,7 @@ public void Configure(IWebHostBuilder builder) .AddErrorDescriber() .AddEntityFrameworkStores() .AddEntityFrameworkStores(); - + services.Configure(options => { options.Password.RequireDigit = false; @@ -45,16 +53,40 @@ public void Configure(IWebHostBuilder builder) options.Password.RequireUppercase = false; options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 1; - + options.SignIn.RequireConfirmedAccount = false; }); - + services.AddAuthentication() + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + ValidateIssuerSigningKey = true, + IssuerSigningKey = + new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Ne!0_0!vzlomayesh!^_^!nikogda!")) + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = c => + { + c.Token = c.Request.Cookies["TemporaryToken"]; + return Task.CompletedTask; + } + }; + }); + + services.Configure(options => { options.CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV3; options.IterationCount = 12000; }); - + services.AddTransient(); services.ConfigureApplicationCookie(options => { @@ -70,16 +102,29 @@ public void Configure(IWebHostBuilder builder) options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; options.SlidingExpiration = true; }); - + services.AddAuthentication() .AddGoogle("Google", options => { options.ClientId = context.Configuration["Authentication:Google:ClientId"]; options.ClientSecret = context.Configuration["Authentication:Google:ClientSecret"]; }); - + services.AddAuthorization(options => { + options.DefaultPolicy = new AuthorizationPolicyBuilder( + JwtBearerDefaults.AuthenticationScheme, + IdentityConstants.ApplicationScheme) + .RequireAuthenticatedUser() + .Build(); + options.AddPolicy( + "Dev", + policyBuilder => + { + policyBuilder.RequireRole("Dev"); + policyBuilder.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme, + JwtBearerDefaults.AuthenticationScheme); + }); options.AddPolicy( "Beta", policyBuilder => @@ -102,7 +147,7 @@ public void Configure(IWebHostBuilder builder) policyBuilder.AddRequirements(new MustOwnPhotoRequirement()); }); }); - + services.AddTransient(serviceProvider => new SimpleEmailSender( serviceProvider.GetRequiredService>(), diff --git a/PhotosApp/Controllers/DevController.cs b/PhotosApp/Controllers/DevController.cs index f2c0e7f6..c8cb6ce5 100644 --- a/PhotosApp/Controllers/DevController.cs +++ b/PhotosApp/Controllers/DevController.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace PhotosApp.Controllers { - [Authorize(Roles = "Dev")] + [Authorize(Policy = "Dev")] public class DevController : Controller { public IActionResult Decode() diff --git a/PhotosApp/Services/TemporaryTokens.cs b/PhotosApp/Services/TemporaryTokens.cs index 09942181..f6eafa6e 100644 --- a/PhotosApp/Services/TemporaryTokens.cs +++ b/PhotosApp/Services/TemporaryTokens.cs @@ -17,13 +17,16 @@ public static string GenerateEncoded() { var claims = new Claim[] { + new Claim(ClaimTypes.NameIdentifier , Guid.NewGuid().ToString()), + new Claim(ClaimsIdentity.DefaultNameClaimType, "User"), + new Claim(ClaimsIdentity.DefaultRoleClaimType, "Dev") }; var jwt = new JwtSecurityToken( claims: claims, - notBefore: null, - expires: null, - signingCredentials: null); + notBefore: DateTime.Now, + expires: DateTime.Now.AddSeconds(30), + signingCredentials: new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256)); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); return encodedJwt; diff --git a/PhotosApp/Views/Shared/_Layout.cshtml b/PhotosApp/Views/Shared/_Layout.cshtml index 5a591365..87650cc3 100644 --- a/PhotosApp/Views/Shared/_Layout.cshtml +++ b/PhotosApp/Views/Shared/_Layout.cshtml @@ -39,12 +39,13 @@ Добавить фото } - @if (User.IsInRole("Dev")) + @if ((await AuthorizationService.AuthorizeAsync(User, "Dev")).Succeeded) { } +
diff --git a/PhotosApp/appsettings.json b/PhotosApp/appsettings.json index 9b5b9db2..21971e6c 100644 --- a/PhotosApp/appsettings.json +++ b/PhotosApp/appsettings.json @@ -14,7 +14,7 @@ "Host": "smtp.gmail.com", "Port": 587, "EnableSSL": true, - "UserName": "your@username.com", + "UserName": "", "Password": "" }, "PhotosService": {