diff --git a/GameLibrary.Tests/Pages/Account/LoginModelTests.cs b/GameLibrary.Tests/Pages/Account/LoginModelTests.cs new file mode 100644 index 0000000..c5c5d8c --- /dev/null +++ b/GameLibrary.Tests/Pages/Account/LoginModelTests.cs @@ -0,0 +1,164 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GameLibrary.Models; +using GameLibrary.Pages.Account; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; + +namespace GameLibrary.Tests.Pages.Account; + +public class LoginModelTests +{ + private readonly Mock> _mockSignInManager; + private readonly Mock> _mockLogger; + private readonly LoginModel _loginModel; + + public LoginModelTests() + { + _mockSignInManager = MockSignInManager(); + _mockLogger = new Mock>(); + _loginModel = new LoginModel(_mockSignInManager.Object, _mockLogger.Object); + } + + private static Mock> MockSignInManager() + { + var userManager = new Mock>( + Mock.Of>(), null!, null!, null!, null!, null!, null!, null!, null!); + + return new Mock>( + userManager.Object, + Mock.Of(), + Mock.Of>(), + null!, null!, null!, null!); + } + + [Fact] + public async Task OnPostAsync_ValidCredentials_RedirectsToReturnUrl() + { + // Arrange + _loginModel.Input = new LoginModel.InputModel + { + Email = "test@example.com", + Password = "Password123!", + RememberMe = false + }; + _mockSignInManager.Setup(s => s.PasswordSignInAsync( + _loginModel.Input.Email, + _loginModel.Input.Password, + _loginModel.Input.RememberMe, + false)) + .ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success); + + // Act + var result = await _loginModel.OnPostAsync("~/"); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("~/", redirectResult.Url); + _mockLogger.Verify( + l => l.Log( + It.Is(logLevel => logLevel == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("User logged in.")), + It.IsAny(), + It.Is>((_, __) => true)), Times.Once); + } + + [Fact] + public async Task OnPostAsync_InvalidCredentials_ReturnsPageWithModelError() + { + // Arrange + _loginModel.Input = new LoginModel.InputModel + { + Email = "test@example.com", + Password = "WrongPassword!", + RememberMe = false + }; + _mockSignInManager.Setup(s => s.PasswordSignInAsync( + _loginModel.Input.Email, + _loginModel.Input.Password, + _loginModel.Input.RememberMe, + false)) + .ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Failed); + + // Act + await _loginModel.OnPostAsync("~/"); + + // Assert + Assert.True(_loginModel.ModelState.ContainsKey(string.Empty)); + Assert.Equal("Invalid login attempt.", _loginModel.ModelState[string.Empty]!.Errors[0].ErrorMessage); + } + + [Fact] + public async Task OnPostAsync_LockedOut_RedirectsToLockoutPage() + { + // Arrange + _loginModel.Input = new LoginModel.InputModel + { + Email = "test@example.com", + Password = "Password123!", + RememberMe = false + }; + _mockSignInManager.Setup(s => s.PasswordSignInAsync( + _loginModel.Input.Email, + _loginModel.Input.Password, + _loginModel.Input.RememberMe, + false)) + .ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.LockedOut); + + // Act + var result = await _loginModel.OnPostAsync("~/"); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("./Lockout", redirectResult.PageName); + _mockLogger.Verify( + l => l.Log( + It.Is(logLevel => logLevel == LogLevel.Warning), + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("User account locked out.")), + It.IsAny(), + It.Is>((_, __) => true)), Times.Once); + } + + [Fact] + public async Task OnPostAsync_TwoFactorRequired_RedirectsTo2faPage() + { + // Arrange + _loginModel.Input = new LoginModel.InputModel + { + Email = "test@example.com", + Password = "Password123!", + RememberMe = true + }; + _mockSignInManager.Setup(s => s.PasswordSignInAsync( + _loginModel.Input.Email, + _loginModel.Input.Password, + _loginModel.Input.RememberMe, + false)) + .ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.TwoFactorRequired); + + // Act + var result = await _loginModel.OnPostAsync("~/"); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("./LoginWith2fa", redirectResult.PageName); + Assert.Equal(new Dictionary { { "ReturnUrl", "~/" }, { "RememberMe", true } }, redirectResult.RouteValues!); + } +} diff --git a/GameLibrary.Tests/Pages/Account/LogoutModelTests.cs b/GameLibrary.Tests/Pages/Account/LogoutModelTests.cs new file mode 100644 index 0000000..cf997c8 --- /dev/null +++ b/GameLibrary.Tests/Pages/Account/LogoutModelTests.cs @@ -0,0 +1,68 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GameLibrary.Models; +using GameLibrary.Pages.Account; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; + +namespace GameLibrary.Tests.Pages.Account; + +public class LogoutModelTests +{ + private readonly Mock> _mockSignInManager; + private readonly Mock> _mockLogger; + private readonly LogoutModel _logoutModel; + + public LogoutModelTests() + { + var userStoreMock = new Mock>(); + var userManagerMock = new Mock>( + userStoreMock.Object, + null!, null!, null!, null!, null!, null!, null!, null!); + + _mockSignInManager = new Mock>( + userManagerMock.Object, + Mock.Of(), + Mock.Of>(), + null!, null!, null!, null!); + + _mockLogger = new Mock>(); + _logoutModel = new LogoutModel(_mockSignInManager.Object, _mockLogger.Object); + } + + [Fact] + public async Task OnPost_NoReturnUrl_RedirectsToPage() + { + // Act + var result = await _logoutModel.OnPost(); + + // Assert + _mockSignInManager.Verify(s => s.SignOutAsync(), Times.Once); + + _mockLogger.Verify( + l => l.Log( + It.Is(logLevel => logLevel == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("User logged out.")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + + Assert.IsType(result); + } +} diff --git a/GameLibrary.Tests/Pages/Account/RegisterModelTests.cs b/GameLibrary.Tests/Pages/Account/RegisterModelTests.cs new file mode 100644 index 0000000..3d7b809 --- /dev/null +++ b/GameLibrary.Tests/Pages/Account/RegisterModelTests.cs @@ -0,0 +1,119 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GameLibrary.Models; +using GameLibrary.Pages.Account; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Moq; + +namespace GameLibrary.Tests.Pages.Account; + +public class RegisterModelTests +{ + private readonly Mock> _mockUserManager; + private readonly Mock> _mockSignInManager; + private readonly Mock> _mockEmailStore; + private readonly Mock> _mockLogger; + private readonly Mock _mockEmailSender; + private readonly RegisterModel _registerModel; + + public RegisterModelTests() + { + _mockEmailStore = new Mock>(); + _mockUserManager = new Mock>( + _mockEmailStore.Object, null!, null!, null!, null!, null!, null!, null!, null!); + + _mockUserManager.Setup(u => u.SupportsUserEmail).Returns(true); + + _mockSignInManager = MockSignInManager(); + _mockLogger = new Mock>(); + _mockEmailSender = new Mock(); + + _registerModel = new RegisterModel( + _mockUserManager.Object, + _mockEmailStore.Object, + _mockSignInManager.Object, + _mockLogger.Object, + _mockEmailSender.Object); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + _registerModel.PageContext.HttpContext = httpContext; + + _registerModel.Url = Mock.Of(); + } + + private static Mock> MockSignInManager() + { + var userManager = new Mock>( + Mock.Of>(), null!, null!, null!, null!, null!, null!, null!, null!); + + return new Mock>( + userManager.Object, + Mock.Of(), + Mock.Of>(), + null!, null!, null!, null!); + } + + [Fact] + public async Task OnPostAsync_WhenUserCreationFails_ReturnsPageWithModelError() + { + // Arrange + _registerModel.Input = new RegisterModel.InputModel + { + Email = "test@example.com", + Password = "Password123!", + ConfirmPassword = "Password123!" + }; + + _mockUserManager.Setup(u => u.CreateAsync(It.IsAny(), _registerModel.Input.Password)) + .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "User creation failed." })); + + // Act + var result = await _registerModel.OnPostAsync("~/"); + + // Assert + Assert.IsType(result); + Assert.True(_registerModel.ModelState.ContainsKey(string.Empty)); + Assert.Equal("User creation failed.", _registerModel.ModelState[string.Empty]!.Errors[0].ErrorMessage); + } + + [Fact] + public async Task OnPostAsync_RegistrationFails_AddsModelError() + { + // Arrange + _registerModel.Input = new RegisterModel.InputModel + { + Email = "test@example.com", + Password = "Password123!", + ConfirmPassword = "Password123!" + }; + + _mockUserManager.Setup(u => u.CreateAsync(It.IsAny(), _registerModel.Input.Password)) + .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "Registration failed." })); + + // Act + var result = await _registerModel.OnPostAsync("~/"); + + // Assert + Assert.IsType(result); + Assert.True(_registerModel.ModelState.ContainsKey(string.Empty)); + Assert.Equal("Registration failed.", _registerModel.ModelState[string.Empty]!.Errors[0].ErrorMessage); + } +} diff --git a/GameLibrary.Tests/ErrorModelTests.cs b/GameLibrary.Tests/Pages/ErrorModelTests.cs similarity index 98% rename from GameLibrary.Tests/ErrorModelTests.cs rename to GameLibrary.Tests/Pages/ErrorModelTests.cs index 16575f7..974f8b2 100644 --- a/GameLibrary.Tests/ErrorModelTests.cs +++ b/GameLibrary.Tests/Pages/ErrorModelTests.cs @@ -1,4 +1,4 @@ -// Copyright 2024 PET Group16 +// Copyright 2024 Web.Tech. Group17 // // Licensed under the Apache License, Version 2.0 (the "License"): // you may not use this file except in compliance with the License. @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +using GameLibrary.Pages; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; using Moq; using System.Diagnostics; -using GameLibrary.Pages; -using Microsoft.AspNetCore.Mvc.RazorPages; namespace GameLibrary.Tests; diff --git a/GameLibrary/Data/ApplicationDbContext.cs b/GameLibrary/Data/ApplicationDbContext.cs index 84ba9a5..50716c8 100644 --- a/GameLibrary/Data/ApplicationDbContext.cs +++ b/GameLibrary/Data/ApplicationDbContext.cs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace GameLibrary.Data; -public class ApplicationDbContext : DbContext +public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions options) : base(options) diff --git a/GameLibrary/Database.db b/GameLibrary/Database.db index 067335c..48beeee 100644 Binary files a/GameLibrary/Database.db and b/GameLibrary/Database.db differ diff --git a/GameLibrary/Database.db-shm b/GameLibrary/Database.db-shm new file mode 100644 index 0000000..fe9ac28 Binary files /dev/null and b/GameLibrary/Database.db-shm differ diff --git a/GameLibrary/Database.db-wal b/GameLibrary/Database.db-wal new file mode 100644 index 0000000..e69de29 diff --git a/GameLibrary/GameLibrary.csproj b/GameLibrary/GameLibrary.csproj index 33f5ba1..5fc27bd 100644 --- a/GameLibrary/GameLibrary.csproj +++ b/GameLibrary/GameLibrary.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,8 +7,14 @@ - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/GameLibrary/Migrations/20241111182014_InitialCreate.Designer.cs b/GameLibrary/Migrations/20241111182014_InitialCreate.Designer.cs new file mode 100644 index 0000000..dfa2300 --- /dev/null +++ b/GameLibrary/Migrations/20241111182014_InitialCreate.Designer.cs @@ -0,0 +1,286 @@ +// +using System; +using GameLibrary.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameLibrary.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20241111182014_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("GameLibrary.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("GameLibrary.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + 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("ProfilePicture") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + 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") + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("GameLibrary.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("GameLibrary.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GameLibrary/Migrations/20241111182014_InitialCreate.cs b/GameLibrary/Migrations/20241111182014_InitialCreate.cs new file mode 100644 index 0000000..7b56f36 --- /dev/null +++ b/GameLibrary/Migrations/20241111182014_InitialCreate.cs @@ -0,0 +1,227 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameLibrary.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + 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), + FirstName = table.Column(type: "TEXT", nullable: true), + LastName = table.Column(type: "TEXT", nullable: true), + Address = table.Column(type: "TEXT", nullable: true), + ProfilePicture = table.Column(type: "TEXT", nullable: true), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "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/GameLibrary/Migrations/ApplicationDbContextModelSnapshot.cs b/GameLibrary/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..4f0fda2 --- /dev/null +++ b/GameLibrary/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,283 @@ +// +using System; +using GameLibrary.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameLibrary.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("GameLibrary.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("GameLibrary.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + 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("ProfilePicture") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + 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") + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("GameLibrary.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("GameLibrary.Models.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("GameLibrary.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GameLibrary/Models/Role.cs b/GameLibrary/Models/Role.cs new file mode 100644 index 0000000..c3b653d --- /dev/null +++ b/GameLibrary/Models/Role.cs @@ -0,0 +1,22 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.AspNetCore.Identity; + +namespace GameLibrary.Models; + +public class Role : IdentityRole +{ + public string? Description { get; set; } +} diff --git a/GameLibrary/Models/User.cs b/GameLibrary/Models/User.cs new file mode 100644 index 0000000..658f428 --- /dev/null +++ b/GameLibrary/Models/User.cs @@ -0,0 +1,25 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.AspNetCore.Identity; + +namespace GameLibrary.Models; + +public class User : IdentityUser +{ + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? Address { get; set; } + public string? ProfilePicture { get; set; } +} diff --git a/GameLibrary/Pages/Account/AccessDenied.cshtml b/GameLibrary/Pages/Account/AccessDenied.cshtml new file mode 100644 index 0000000..017f6ff --- /dev/null +++ b/GameLibrary/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/GameLibrary/Pages/Account/AccessDenied.cshtml.cs b/GameLibrary/Pages/Account/AccessDenied.cshtml.cs new file mode 100644 index 0000000..5c9c6c3 --- /dev/null +++ b/GameLibrary/Pages/Account/AccessDenied.cshtml.cs @@ -0,0 +1,34 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account; + +/// +/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public class AccessDeniedModel : PageModel +{ + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public void OnGet() + { + } +} diff --git a/GameLibrary/Pages/Account/ConfirmEmail.cshtml b/GameLibrary/Pages/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000..2deb2e5 --- /dev/null +++ b/GameLibrary/Pages/Account/ConfirmEmail.cshtml @@ -0,0 +1,8 @@ +@page +@model ConfirmEmailModel +@{ + ViewData["Title"] = "Confirm email"; +} + +

@ViewData["Title"]

+ diff --git a/GameLibrary/Pages/Account/ConfirmEmail.cshtml.cs b/GameLibrary/Pages/Account/ConfirmEmail.cshtml.cs new file mode 100644 index 0000000..8ce7171 --- /dev/null +++ b/GameLibrary/Pages/Account/ConfirmEmail.cshtml.cs @@ -0,0 +1,59 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using System.Text; + +namespace GameLibrary.Pages.Account; + +public class ConfirmEmailModel : PageModel +{ + private readonly UserManager _userManager; + + public ConfirmEmailModel(UserManager userManager) + { + _userManager = userManager; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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/GameLibrary/Pages/Account/ConfirmEmailChange.cshtml b/GameLibrary/Pages/Account/ConfirmEmailChange.cshtml new file mode 100644 index 0000000..114fa88 --- /dev/null +++ b/GameLibrary/Pages/Account/ConfirmEmailChange.cshtml @@ -0,0 +1,8 @@ +@page +@model ConfirmEmailChangeModel +@{ + ViewData["Title"] = "Confirm email change"; +} + +

@ViewData["Title"]

+ diff --git a/GameLibrary/Pages/Account/ConfirmEmailChange.cshtml.cs b/GameLibrary/Pages/Account/ConfirmEmailChange.cshtml.cs new file mode 100644 index 0000000..896569b --- /dev/null +++ b/GameLibrary/Pages/Account/ConfirmEmailChange.cshtml.cs @@ -0,0 +1,78 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using System.Text; + +namespace GameLibrary.Pages.Account; + +public class ConfirmEmailChangeModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ConfirmEmailChangeModel(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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/GameLibrary/Pages/Account/ForgotPassword.cshtml b/GameLibrary/Pages/Account/ForgotPassword.cshtml new file mode 100644 index 0000000..602e25f --- /dev/null +++ b/GameLibrary/Pages/Account/ForgotPassword.cshtml @@ -0,0 +1,22 @@ +@page +@model ForgotPasswordModel +@{ + ViewData["Title"] = "Forgot your password?"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+
+ +
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/ForgotPassword.cshtml.cs b/GameLibrary/Pages/Account/ForgotPassword.cshtml.cs new file mode 100644 index 0000000..3a8683b --- /dev/null +++ b/GameLibrary/Pages/Account/ForgotPassword.cshtml.cs @@ -0,0 +1,93 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; + +namespace GameLibrary.Pages.Account; + +public class ForgotPasswordModel : PageModel +{ + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + + public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) + { + _userManager = userManager; + _emailSender = emailSender; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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 = "", 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/GameLibrary/Pages/Account/ForgotPasswordConfirmation.cshtml b/GameLibrary/Pages/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000..4f5ae99 --- /dev/null +++ b/GameLibrary/Pages/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,10 @@ +@page +@model ForgotPasswordConfirmation +@{ + ViewData["Title"] = "Forgot password confirmation"; +} + +

@ViewData["Title"]

+

+ Please check your email to reset your password. +

diff --git a/GameLibrary/Pages/Account/ForgotPasswordConfirmation.cshtml.cs b/GameLibrary/Pages/Account/ForgotPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000..96804a5 --- /dev/null +++ b/GameLibrary/Pages/Account/ForgotPasswordConfirmation.cshtml.cs @@ -0,0 +1,36 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account; + +/// +/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +[AllowAnonymous] +public class ForgotPasswordConfirmation : PageModel +{ + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public void OnGet() + { + } +} diff --git a/GameLibrary/Pages/Account/Lockout.cshtml b/GameLibrary/Pages/Account/Lockout.cshtml new file mode 100644 index 0000000..4eded88 --- /dev/null +++ b/GameLibrary/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/GameLibrary/Pages/Account/Lockout.cshtml.cs b/GameLibrary/Pages/Account/Lockout.cshtml.cs new file mode 100644 index 0000000..e40ca8a --- /dev/null +++ b/GameLibrary/Pages/Account/Lockout.cshtml.cs @@ -0,0 +1,36 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account; + +/// +/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +[AllowAnonymous] +public class LockoutModel : PageModel +{ + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public void OnGet() + { + } +} diff --git a/GameLibrary/Pages/Account/Login.cshtml b/GameLibrary/Pages/Account/Login.cshtml new file mode 100644 index 0000000..228743d --- /dev/null +++ b/GameLibrary/Pages/Account/Login.cshtml @@ -0,0 +1,46 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + +
+
+

Login

+

Please enter your login and password!

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

Don't have an account? + Sign Up +

+
+
+
diff --git a/GameLibrary/Pages/Account/Login.cshtml.cs b/GameLibrary/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..b25e633 --- /dev/null +++ b/GameLibrary/Pages/Account/Login.cshtml.cs @@ -0,0 +1,145 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.Pages.Account; + +public class LoginModel : PageModel +{ + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public IList ExternalLogins { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string ReturnUrl { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string ErrorMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [EmailAddress] + public string Email { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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/GameLibrary/Pages/Account/LoginWith2fa.cshtml b/GameLibrary/Pages/Account/LoginWith2fa.cshtml new file mode 100644 index 0000000..984df4c --- /dev/null +++ b/GameLibrary/Pages/Account/LoginWith2fa.cshtml @@ -0,0 +1,35 @@ +@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. +

diff --git a/GameLibrary/Pages/Account/LoginWith2fa.cshtml.cs b/GameLibrary/Pages/Account/LoginWith2fa.cshtml.cs new file mode 100644 index 0000000..a164afe --- /dev/null +++ b/GameLibrary/Pages/Account/LoginWith2fa.cshtml.cs @@ -0,0 +1,138 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.Pages.Account; + +public class LoginWith2faModel : PageModel +{ + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public LoginWith2faModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool RememberMe { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string ReturnUrl { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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); + + var userId = await _userManager.GetUserIdAsync(user); + + 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/GameLibrary/Pages/Account/LoginWithRecoveryCode.cshtml b/GameLibrary/Pages/Account/LoginWithRecoveryCode.cshtml new file mode 100644 index 0000000..5ae3cab --- /dev/null +++ b/GameLibrary/Pages/Account/LoginWithRecoveryCode.cshtml @@ -0,0 +1,25 @@ +@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. +

+
+
+
+ +
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/LoginWithRecoveryCode.cshtml.cs b/GameLibrary/Pages/Account/LoginWithRecoveryCode.cshtml.cs new file mode 100644 index 0000000..80ded71 --- /dev/null +++ b/GameLibrary/Pages/Account/LoginWithRecoveryCode.cshtml.cs @@ -0,0 +1,121 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.Pages.Account; + +public class LoginWithRecoveryCodeModel : PageModel +{ + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public LoginWithRecoveryCodeModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string ReturnUrl { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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); + + var userId = await _userManager.GetUserIdAsync(user); + + 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 account locked out."); + 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/GameLibrary/Pages/Account/Logout.cshtml b/GameLibrary/Pages/Account/Logout.cshtml new file mode 100644 index 0000000..2f7ac1a --- /dev/null +++ b/GameLibrary/Pages/Account/Logout.cshtml @@ -0,0 +1,21 @@ +@page +@model LogoutModel +@{ + ViewData["Title"] = "Log out"; +} + +
+

@ViewData["Title"]

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

You have successfully logged out of the application.

+ } + } +
diff --git a/GameLibrary/Pages/Account/Logout.cshtml.cs b/GameLibrary/Pages/Account/Logout.cshtml.cs new file mode 100644 index 0000000..692f8d0 --- /dev/null +++ b/GameLibrary/Pages/Account/Logout.cshtml.cs @@ -0,0 +1,50 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account; + +public class LogoutModel : PageModel +{ + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LogoutModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + public async Task OnPost(string returnUrl = null) + { + await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out."); + if (returnUrl != null) + { + return LocalRedirect(returnUrl); + } + else + { + // This needs to be a redirect so that the browser performs a new + // request and the identity for the user gets updated. + return RedirectToPage(); + } + } +} diff --git a/GameLibrary/Pages/Account/Manage/ChangePassword.cshtml b/GameLibrary/Pages/Account/Manage/ChangePassword.cshtml new file mode 100644 index 0000000..13d3823 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/ChangePassword.cshtml @@ -0,0 +1,32 @@ +@page +@model ChangePasswordModel +@{ + ViewData["Title"] = "Change password"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

@ViewData["Title"]

+ +
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/Manage/ChangePassword.cshtml.cs b/GameLibrary/Pages/Account/Manage/ChangePassword.cshtml.cs new file mode 100644 index 0000000..b74c761 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -0,0 +1,136 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string StatusMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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/GameLibrary/Pages/Account/Manage/DeletePersonalData.cshtml b/GameLibrary/Pages/Account/Manage/DeletePersonalData.cshtml new file mode 100644 index 0000000..6b1a539 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/DeletePersonalData.cshtml @@ -0,0 +1,29 @@ +@page +@model DeletePersonalDataModel +@{ + ViewData["Title"] = "Delete Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ + + +
+
+ + @if (Model.RequirePassword) + { +
+ + + +
+ } + +
+
diff --git a/GameLibrary/Pages/Account/Manage/DeletePersonalData.cshtml.cs b/GameLibrary/Pages/Account/Manage/DeletePersonalData.cshtml.cs new file mode 100644 index 0000000..5ed362e --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/DeletePersonalData.cshtml.cs @@ -0,0 +1,112 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + 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."); + } + + await _signInManager.SignOutAsync(); + + _logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + return Redirect("~/"); + } +} diff --git a/GameLibrary/Pages/Account/Manage/Disable2fa.cshtml b/GameLibrary/Pages/Account/Manage/Disable2fa.cshtml new file mode 100644 index 0000000..31ecb7e --- /dev/null +++ b/GameLibrary/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/GameLibrary/Pages/Account/Manage/Disable2fa.cshtml.cs b/GameLibrary/Pages/Account/Manage/Disable2fa.cshtml.cs new file mode 100644 index 0000000..5502e3c --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/Disable2fa.cshtml.cs @@ -0,0 +1,78 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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 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."); + } + + _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"); + } +} diff --git a/GameLibrary/Pages/Account/Manage/DownloadPersonalData.cshtml b/GameLibrary/Pages/Account/Manage/DownloadPersonalData.cshtml new file mode 100644 index 0000000..dfab4cb --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/DownloadPersonalData.cshtml @@ -0,0 +1,8 @@ +@page +@model DownloadPersonalDataModel +@{ + ViewData["Title"] = "Download Your Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

diff --git a/GameLibrary/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/GameLibrary/Pages/Account/Manage/DownloadPersonalData.cshtml.cs new file mode 100644 index 0000000..85ca57a --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/DownloadPersonalData.cshtml.cs @@ -0,0 +1,73 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Text.Json; + +namespace GameLibrary.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 IActionResult OnGet() + { + return NotFound(); + } + + 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(User).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); + } + + personalData.Add($"Authenticator Key", await _userManager.GetAuthenticatorKeyAsync(user)); + + Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); + return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json"); + } +} diff --git a/GameLibrary/Pages/Account/Manage/Email.cshtml b/GameLibrary/Pages/Account/Manage/Email.cshtml new file mode 100644 index 0000000..038a0fc --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/Email.cshtml @@ -0,0 +1,40 @@ +@page +@model EmailModel +@{ + ViewData["Title"] = "Manage Email"; + ViewData["ActivePage"] = ManageNavPages.Email; +} + +

@ViewData["Title"]

+ +
+
+
+ + @if (Model.IsEmailConfirmed) + { +
+ +
+ +
+ +
+ } + else + { +
+ + + +
+ } +
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/Manage/Email.cshtml.cs b/GameLibrary/Pages/Account/Manage/Email.cshtml.cs new file mode 100644 index 0000000..8829738 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/Email.cshtml.cs @@ -0,0 +1,181 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; + +namespace GameLibrary.Pages.Account.Manage; + +public 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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string Email { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool IsEmailConfirmed { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string StatusMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [EmailAddress] + [Display(Name = "New email")] + public string NewEmail { get; set; } + } + + private async Task LoadAsync(User 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 { area = "", 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 = "", 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/GameLibrary/Pages/Account/Manage/EnableAuthenticator.cshtml b/GameLibrary/Pages/Account/Manage/EnableAuthenticator.cshtml new file mode 100644 index 0000000..0097109 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,49 @@ +@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. +
+
diff --git a/GameLibrary/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/GameLibrary/Pages/Account/Manage/EnableAuthenticator.cshtml.cs new file mode 100644 index 0000000..d3839a9 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -0,0 +1,196 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Text; +using System.Text.Encodings.Web; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string SharedKey { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string AuthenticatorUri { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string[] RecoveryCodes { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string StatusMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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 hyphens + 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(User 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.AsSpan(currentPosition, 4)).Append(' '); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + CultureInfo.InvariantCulture, + AuthenticatorUriFormat, + _urlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"), + _urlEncoder.Encode(email), + unformattedKey); + } +} diff --git a/GameLibrary/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/GameLibrary/Pages/Account/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 0000000..91e1d01 --- /dev/null +++ b/GameLibrary/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"]

+ +
+
+ +
+
diff --git a/GameLibrary/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/GameLibrary/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs new file mode 100644 index 0000000..7622476 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -0,0 +1,90 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string[] RecoveryCodes { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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) + { + throw new InvalidOperationException($"Cannot generate recovery codes for user 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 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"); + } +} diff --git a/GameLibrary/Pages/Account/Manage/Index.cshtml b/GameLibrary/Pages/Account/Manage/Index.cshtml new file mode 100644 index 0000000..d774475 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/Index.cshtml @@ -0,0 +1,26 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Profile"; + ViewData["ActivePage"] = ManageNavPages.Index; +} + +

@ViewData["Title"]

+ +
+
+
+ +
+ + +
+
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/Manage/Index.cshtml.cs b/GameLibrary/Pages/Account/Manage/Index.cshtml.cs new file mode 100644 index 0000000..693c235 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/Index.cshtml.cs @@ -0,0 +1,127 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.Pages.Account.Manage; + +public class IndexModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public IndexModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string Username { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string StatusMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } + + private async Task LoadAsync(User 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/GameLibrary/Pages/Account/Manage/ManageNavPages.cs b/GameLibrary/Pages/Account/Manage/ManageNavPages.cs new file mode 100644 index 0000000..4ef9be9 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/ManageNavPages.cs @@ -0,0 +1,133 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace GameLibrary.Pages.Account.Manage; + +/// +/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public static class ManageNavPages +{ + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string Index => "Index"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string Email => "Email"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ChangePassword => "ChangePassword"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DownloadPersonalData => "DownloadPersonalData"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DeletePersonalData => "DeletePersonalData"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ExternalLogins => "ExternalLogins"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string PersonalData => "PersonalData"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public 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/GameLibrary/Pages/Account/Manage/PersonalData.cshtml b/GameLibrary/Pages/Account/Manage/PersonalData.cshtml new file mode 100644 index 0000000..121b109 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/PersonalData.cshtml @@ -0,0 +1,23 @@ +@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 +

+
+
diff --git a/GameLibrary/Pages/Account/Manage/PersonalData.cshtml.cs b/GameLibrary/Pages/Account/Manage/PersonalData.cshtml.cs new file mode 100644 index 0000000..8672655 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/PersonalData.cshtml.cs @@ -0,0 +1,45 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.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(); + } +} diff --git a/GameLibrary/Pages/Account/Manage/ResetAuthenticator.cshtml b/GameLibrary/Pages/Account/Manage/ResetAuthenticator.cshtml new file mode 100644 index 0000000..f3297e0 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,24 @@ +@page +@model ResetAuthenticatorModel +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ +
+
+ +
+
diff --git a/GameLibrary/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/GameLibrary/Pages/Account/Manage/ResetAuthenticator.cshtml.cs new file mode 100644 index 0000000..14feb0b --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -0,0 +1,76 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account.Manage; + +public class ResetAuthenticatorModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public ResetAuthenticatorModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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); + var userId = await _userManager.GetUserIdAsync(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"); + } +} diff --git a/GameLibrary/Pages/Account/Manage/SetPassword.cshtml b/GameLibrary/Pages/Account/Manage/SetPassword.cshtml new file mode 100644 index 0000000..a6d1118 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/SetPassword.cshtml @@ -0,0 +1,31 @@ +@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. +

+
+
+
+ +
+ + + +
+
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/Manage/SetPassword.cshtml.cs b/GameLibrary/Pages/Account/Manage/SetPassword.cshtml.cs new file mode 100644 index 0000000..5614141 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/SetPassword.cshtml.cs @@ -0,0 +1,124 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string StatusMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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/GameLibrary/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/GameLibrary/Pages/Account/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000..49a8518 --- /dev/null +++ b/GameLibrary/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]
+ } +
+
diff --git a/GameLibrary/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs b/GameLibrary/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs new file mode 100644 index 0000000..b74b0fc --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs @@ -0,0 +1,55 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account.Manage; + +/// +/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public class ShowRecoveryCodesModel : PageModel +{ + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string[] RecoveryCodes { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string StatusMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public IActionResult OnGet() + { + if (RecoveryCodes == null || RecoveryCodes.Length == 0) + { + return RedirectToPage("./TwoFactorAuthentication"); + } + + return Page(); + } +} diff --git a/GameLibrary/Pages/Account/Manage/TwoFactorAuthentication.cshtml b/GameLibrary/Pages/Account/Manage/TwoFactorAuthentication.cshtml new file mode 100644 index 0000000..accbc2f --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/TwoFactorAuthentication.cshtml @@ -0,0 +1,67 @@ +@page +@using Microsoft.AspNetCore.Http.Features +@model TwoFactorAuthenticationModel +@{ + ViewData["Title"] = "Two-factor authentication (2FA)"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+@{ + var consentFeature = HttpContext.Features.Get(); + @if (consentFeature?.CanTrack ?? true) + { + @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 + { + Set up authenticator app + Reset authenticator app + } + } + else + { +
+ Privacy and cookie policy have not been accepted. +

You must accept the policy before you can enable two factor authentication.

+
+ } +} diff --git a/GameLibrary/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs b/GameLibrary/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs new file mode 100644 index 0000000..e040e5e --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs @@ -0,0 +1,98 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account.Manage; + +public class TwoFactorAuthenticationModel : PageModel +{ + 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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool HasAuthenticator { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public int RecoveryCodesLeft { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public bool Is2faEnabled { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool IsMachineRemembered { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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)}'."); + } + + 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 OnPostAsync() + { + 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(); + } +} diff --git a/GameLibrary/Pages/Account/Manage/_Layout.cshtml b/GameLibrary/Pages/Account/Manage/_Layout.cshtml new file mode 100644 index 0000000..94b2ea7 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/_Layout.cshtml @@ -0,0 +1,29 @@ +@{ + if (ViewData.TryGetValue("ParentLayout", out var parentLayout) && parentLayout != null) + { + Layout = parentLayout.ToString(); + } + else + { + Layout = "/Pages/Shared/_Layout.cshtml"; + } +} + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} diff --git a/GameLibrary/Pages/Account/Manage/_ManageNav.cshtml b/GameLibrary/Pages/Account/Manage/_ManageNav.cshtml new file mode 100644 index 0000000..6d2863c --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/_ManageNav.cshtml @@ -0,0 +1,15 @@ +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + diff --git a/GameLibrary/Pages/Account/Manage/_StatusMessage.cshtml b/GameLibrary/Pages/Account/Manage/_StatusMessage.cshtml new file mode 100644 index 0000000..c898543 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/GameLibrary/Pages/Account/Manage/_ViewImports.cshtml b/GameLibrary/Pages/Account/Manage/_ViewImports.cshtml new file mode 100644 index 0000000..929f2f6 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using GameLibrary.Pages.Account.Manage diff --git a/GameLibrary/Pages/Account/Register.cshtml b/GameLibrary/Pages/Account/Register.cshtml new file mode 100644 index 0000000..03e5c1f --- /dev/null +++ b/GameLibrary/Pages/Account/Register.cshtml @@ -0,0 +1,43 @@ +@page +@model RegisterModel +@{ + ViewData["Title"] = "Register"; +} + +
+
+

Register

+

Create a new account to get started!

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

+ Already have an account? + Log In +

+
+
+
diff --git a/GameLibrary/Pages/Account/Register.cshtml.cs b/GameLibrary/Pages/Account/Register.cshtml.cs new file mode 100644 index 0000000..195df06 --- /dev/null +++ b/GameLibrary/Pages/Account/Register.cshtml.cs @@ -0,0 +1,185 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +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.AspNetCore.WebUtilities; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; + +namespace GameLibrary.Pages.Account; + +public class RegisterModel : PageModel +{ + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly IUserStore _userStore; + private readonly IUserEmailStore _emailStore; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + + public RegisterModel( + UserManager userManager, + IUserStore userStore, + SignInManager signInManager, + ILogger logger, + IEmailSender emailSender) + { + _userManager = userManager; + _userStore = userStore; + _emailStore = GetEmailStore(); + _signInManager = signInManager; + _logger = logger; + _emailSender = emailSender; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string ReturnUrl { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public IList ExternalLogins { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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 = CreateUser(); + + await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); + await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); + var result = await _userManager.CreateAsync(user, Input.Password); + + if (result.Succeeded) + { + _logger.LogInformation("User created a new account with password."); + + 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 = "", userId = userId, 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(); + } + + private User CreateUser() + { + try + { + return Activator.CreateInstance(); + } + catch + { + throw new InvalidOperationException($"Can't create an instance of '{nameof(User)}'. " + + $"Ensure that '{nameof(User)}' is not an abstract class and has a parameterless constructor, or alternatively " + + $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml"); + } + } + + private IUserEmailStore GetEmailStore() + { + if (!_userManager.SupportsUserEmail) + { + throw new NotSupportedException("The default UI requires a user store with email support."); + } + return (IUserEmailStore) _userStore; + } +} diff --git a/GameLibrary/Pages/Account/RegisterConfirmation.cshtml b/GameLibrary/Pages/Account/RegisterConfirmation.cshtml new file mode 100644 index 0000000..d3e7f61 --- /dev/null +++ b/GameLibrary/Pages/Account/RegisterConfirmation.cshtml @@ -0,0 +1,23 @@ +@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/GameLibrary/Pages/Account/RegisterConfirmation.cshtml.cs b/GameLibrary/Pages/Account/RegisterConfirmation.cshtml.cs new file mode 100644 index 0000000..e8ae5a1 --- /dev/null +++ b/GameLibrary/Pages/Account/RegisterConfirmation.cshtml.cs @@ -0,0 +1,89 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +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 System.Text; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string Email { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool DisplayConfirmAccountLink { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string EmailConfirmationUrl { get; set; } + + public async Task OnGetAsync(string email, string returnUrl = null) + { + if (email == null) + { + return RedirectToPage("/Index"); + } + returnUrl = returnUrl ?? Url.Content("~/"); + + 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 = "", userId = userId, code = code, returnUrl = returnUrl }, + protocol: Request.Scheme); + } + + return Page(); + } +} diff --git a/GameLibrary/Pages/Account/ResendEmailConfirmation.cshtml b/GameLibrary/Pages/Account/ResendEmailConfirmation.cshtml new file mode 100644 index 0000000..8b69c2d --- /dev/null +++ b/GameLibrary/Pages/Account/ResendEmailConfirmation.cshtml @@ -0,0 +1,22 @@ +@page +@model ResendEmailConfirmationModel +@{ + ViewData["Title"] = "Resend email confirmation"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+
+ +
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/ResendEmailConfirmation.cshtml.cs b/GameLibrary/Pages/Account/ResendEmailConfirmation.cshtml.cs new file mode 100644 index 0000000..e7bdc98 --- /dev/null +++ b/GameLibrary/Pages/Account/ResendEmailConfirmation.cshtml.cs @@ -0,0 +1,98 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +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 System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; + +namespace GameLibrary.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; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [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/GameLibrary/Pages/Account/ResetPassword.cshtml b/GameLibrary/Pages/Account/ResetPassword.cshtml new file mode 100644 index 0000000..bb67fb5 --- /dev/null +++ b/GameLibrary/Pages/Account/ResetPassword.cshtml @@ -0,0 +1,33 @@ +@page +@model ResetPasswordModel +@{ + ViewData["Title"] = "Reset password"; +} + +

@ViewData["Title"]

+

Reset your password.

+
+
+
+
+ + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
diff --git a/GameLibrary/Pages/Account/ResetPassword.cshtml.cs b/GameLibrary/Pages/Account/ResetPassword.cshtml.cs new file mode 100644 index 0000000..0b6f690 --- /dev/null +++ b/GameLibrary/Pages/Account/ResetPassword.cshtml.cs @@ -0,0 +1,126 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace GameLibrary.Pages.Account; + +public class ResetPasswordModel : PageModel +{ + private readonly UserManager _userManager; + + public ResetPasswordModel(UserManager userManager) + { + _userManager = userManager; + } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + [EmailAddress] + public string Email { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [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; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [Required] + 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/GameLibrary/Pages/Account/ResetPasswordConfirmation.cshtml b/GameLibrary/Pages/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000..c52552f --- /dev/null +++ b/GameLibrary/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/GameLibrary/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/GameLibrary/Pages/Account/ResetPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000..147569b --- /dev/null +++ b/GameLibrary/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -0,0 +1,36 @@ +// Copyright 2024 Web.Tech. Group17 +// +// Licensed under the Apache License, Version 2.0 (the "License"): +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable disable + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace GameLibrary.Pages.Account; + +/// +/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +[AllowAnonymous] +public class ResetPasswordConfirmationModel : PageModel +{ + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public void OnGet() + { + } +} diff --git a/GameLibrary/Pages/Account/_StatusMessage.cshtml b/GameLibrary/Pages/Account/_StatusMessage.cshtml new file mode 100644 index 0000000..c898543 --- /dev/null +++ b/GameLibrary/Pages/Account/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/GameLibrary/Pages/Account/_ViewImports.cshtml b/GameLibrary/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000..6c38399 --- /dev/null +++ b/GameLibrary/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using GameLibrary.Pages.Account diff --git a/GameLibrary/Pages/Index.cshtml.cs b/GameLibrary/Pages/Index.cshtml.cs index 109b51a..35000e6 100644 --- a/GameLibrary/Pages/Index.cshtml.cs +++ b/GameLibrary/Pages/Index.cshtml.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace GameLibrary.Pages; diff --git a/GameLibrary/Pages/Privacy.cshtml.cs b/GameLibrary/Pages/Privacy.cshtml.cs index f956589..d90958f 100644 --- a/GameLibrary/Pages/Privacy.cshtml.cs +++ b/GameLibrary/Pages/Privacy.cshtml.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace GameLibrary.Pages; diff --git a/GameLibrary/Pages/Shared/_Layout.cshtml b/GameLibrary/Pages/Shared/_Layout.cshtml index 6290e2c..38020a3 100644 --- a/GameLibrary/Pages/Shared/_Layout.cshtml +++ b/GameLibrary/Pages/Shared/_Layout.cshtml @@ -8,34 +8,34 @@ -
-