From 04877691b9ecfa7b4c368b73f6b59e3fc7c16717 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 18 Aug 2026 14:56:41 +0200 Subject: [PATCH] Add protected admin scope with login --- RazorPagesMovie/Models/AdminUser.cs | 15 ++++++ RazorPagesMovie/Pages/Admin/Index.cshtml | 11 +++++ RazorPagesMovie/Pages/Admin/Index.cshtml.cs | 19 ++++++++ RazorPagesMovie/Pages/Login.cshtml | 28 +++++++++++ RazorPagesMovie/Pages/Login.cshtml.cs | 53 +++++++++++++++++++++ RazorPagesMovie/Program.cs | 16 ++++++- RazorPagesMovie/SeedData/SeedData.cs | 45 +++++++++++++---- 7 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 RazorPagesMovie/Models/AdminUser.cs create mode 100644 RazorPagesMovie/Pages/Admin/Index.cshtml create mode 100644 RazorPagesMovie/Pages/Admin/Index.cshtml.cs create mode 100644 RazorPagesMovie/Pages/Login.cshtml create mode 100644 RazorPagesMovie/Pages/Login.cshtml.cs diff --git a/RazorPagesMovie/Models/AdminUser.cs b/RazorPagesMovie/Models/AdminUser.cs new file mode 100644 index 0000000..32db988 --- /dev/null +++ b/RazorPagesMovie/Models/AdminUser.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace RazorPagesMovie.Models; + +public class AdminUser +{ + public int Id { get; set; } + + [Required] + [StringLength(100)] + public string Username { get; set; } = string.Empty; + + [Required] + public string PasswordHash { get; set; } = string.Empty; +} diff --git a/RazorPagesMovie/Pages/Admin/Index.cshtml b/RazorPagesMovie/Pages/Admin/Index.cshtml new file mode 100644 index 0000000..5ee48bb --- /dev/null +++ b/RazorPagesMovie/Pages/Admin/Index.cshtml @@ -0,0 +1,11 @@ +@page +@model RazorPagesMovie.Pages.Admin.IndexModel +@{ + ViewData["Title"] = "Admin"; +} + +

Welcome, @User.Identity?.Name

+ +
+ +
diff --git a/RazorPagesMovie/Pages/Admin/Index.cshtml.cs b/RazorPagesMovie/Pages/Admin/Index.cshtml.cs new file mode 100644 index 0000000..f6ee3b2 --- /dev/null +++ b/RazorPagesMovie/Pages/Admin/Index.cshtml.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesMovie.Pages.Admin; + +public class IndexModel : PageModel +{ + public void OnGet() + { + } + + public async Task OnPostLogoutAsync() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return RedirectToPage("/Login"); + } +} diff --git a/RazorPagesMovie/Pages/Login.cshtml b/RazorPagesMovie/Pages/Login.cshtml new file mode 100644 index 0000000..dbfa33b --- /dev/null +++ b/RazorPagesMovie/Pages/Login.cshtml @@ -0,0 +1,28 @@ +@page +@model RazorPagesMovie.Pages.LoginModel +@{ + ViewData["Title"] = "Login"; +} + +
+
+

Admin Login

+ + @if (!string.IsNullOrEmpty(Model.ErrorMessage)) + { +
@Model.ErrorMessage
+ } + +
+
+ + +
+
+ + +
+ +
+
+
diff --git a/RazorPagesMovie/Pages/Login.cshtml.cs b/RazorPagesMovie/Pages/Login.cshtml.cs new file mode 100644 index 0000000..d7b9496 --- /dev/null +++ b/RazorPagesMovie/Pages/Login.cshtml.cs @@ -0,0 +1,53 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using RazorPagesMovie.Data; +using RazorPagesMovie.Models; + +namespace RazorPagesMovie.Pages; + +public class LoginModel : PageModel +{ + private readonly RazorPagesMovieContext _context; + + public LoginModel(RazorPagesMovieContext context) + { + _context = context; + } + + [BindProperty] + public string Username { get; set; } = string.Empty; + + [BindProperty] + public string Password { get; set; } = string.Empty; + + public string? ErrorMessage { get; set; } + + public void OnGet() + { + } + + public async Task OnPostAsync(string? returnUrl) + { + var user = await _context.AdminUser.SingleOrDefaultAsync(u => u.Username == Username); + var hasher = new PasswordHasher(); + + if (user is null || hasher.VerifyHashedPassword(user, user.PasswordHash, Password) == PasswordVerificationResult.Failed) + { + ErrorMessage = "Benutzername oder Passwort ist falsch."; + return Page(); + } + + var identity = new ClaimsIdentity( + new[] { new Claim(ClaimTypes.Name, user.Username) }, + CookieAuthenticationDefaults.AuthenticationScheme); + + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); + + return LocalRedirect(returnUrl ?? "/Admin"); + } +} diff --git a/RazorPagesMovie/Program.cs b/RazorPagesMovie/Program.cs index 36d37a9..df4a20b 100644 --- a/RazorPagesMovie/Program.cs +++ b/RazorPagesMovie/Program.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; using RazorPagesMovie.Data; using RazorPagesMovie.Models; @@ -6,7 +7,19 @@ var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found."))); -builder.Services.AddRazorPages(); + +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.LoginPath = "/Login"; + options.AccessDeniedPath = "/Login"; + }); +builder.Services.AddAuthorization(); + +builder.Services.AddRazorPages(options => +{ + options.Conventions.AuthorizeFolder("/Admin"); +}); var app = builder.Build(); @@ -27,6 +40,7 @@ if (!app.Environment.IsDevelopment()) app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); +app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); diff --git a/RazorPagesMovie/SeedData/SeedData.cs b/RazorPagesMovie/SeedData/SeedData.cs index 2d4ed1a..d3ae35a 100644 --- a/RazorPagesMovie/SeedData/SeedData.cs +++ b/RazorPagesMovie/SeedData/SeedData.cs @@ -1,4 +1,6 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using RazorPagesMovie.Data; namespace RazorPagesMovie.Models; @@ -15,18 +17,45 @@ public static class SeedData throw new ArgumentNullException("Null RazorPagesMovieContext"); } - if (context.Movie.Any()) + if (!context.Movie.Any()) { - return; - } - - context.Movie.AddRange( + context.Movie.AddRange( new Movie { Title = "When Harry Met Sally", ReleaseDate = DateTime.Parse("1989-2-12"), Genre = "Romantic Comedy", Price = 7.99M, Rating = "R" }, new Movie { Title = "Ghostbusters", ReleaseDate = DateTime.Parse("1984-3-13"), Genre = "Comedy", Price = 8.99M, Rating = "G" }, new Movie { Title = "Ghostbusters 2", ReleaseDate = DateTime.Parse("1986-2-23"), Genre = "Comedy", Price = 9.99M, Rating = "G" }, new Movie { Title = "Rio Bravo", ReleaseDate = DateTime.Parse("1959-4-15"), Genre = "Western", Price = 3.99M, Rating = "NR" } - ); - context.SaveChanges(); + ); + context.SaveChanges(); + } + + if (!context.AdminUser.Any()) + { + var accounts = serviceProvider.GetRequiredService() + .GetSection("AdminAccounts") + .Get>() ?? new List(); + + var hasher = new PasswordHasher(); + + foreach (var account in accounts) + { + if (string.IsNullOrWhiteSpace(account.Username) || string.IsNullOrWhiteSpace(account.Password)) + { + continue; + } + + var user = new AdminUser { Username = account.Username }; + user.PasswordHash = hasher.HashPassword(user, account.Password); + context.AdminUser.Add(user); + } + + context.SaveChanges(); + } } } + + private class AdminAccountSeed + { + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + } } \ No newline at end of file