diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs new file mode 100644 index 0000000..4598150 --- /dev/null +++ b/Controllers/AccountController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace NoticeBoard.Controllers; + +public class AccountController : Controller +{ + private readonly IConfiguration _config; + + public AccountController(IConfiguration config) + { + _config = config; + } + + [HttpGet] + public IActionResult Login(string? returnUrl = null) + { + if (User.Identity?.IsAuthenticated == true) + return RedirectToAction("Index", "Admin"); + + ViewBag.ReturnUrl = returnUrl; + return View(); + } + + [HttpPost] + public async Task Login(string username, string password, string? returnUrl = null) + { + var adminUser = _config["Admin:Username"] ?? "admin"; + var adminPass = _config["Admin:Password"] ?? "admin"; + + if (username == adminUser && password == adminPass) + { + var claims = new List + { + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Role, "Admin") + }; + + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); + + if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl)) + return Redirect(returnUrl); + + return RedirectToAction("Index", "Admin"); + } + + ViewBag.Error = "Invalid username or password."; + ViewBag.ReturnUrl = returnUrl; + return View(); + } + + [HttpGet] + public async Task Logout() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return RedirectToAction("Login"); + } +} diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs index dd35ae2..40fe4c3 100644 --- a/Controllers/AdminController.cs +++ b/Controllers/AdminController.cs @@ -1,9 +1,11 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NoticeBoard.Data; namespace NoticeBoard.Controllers; +[Authorize] public class AdminController : Controller { private readonly AppDbContext _db; diff --git a/Controllers/SlidesController.cs b/Controllers/SlidesController.cs index fa76e97..29cf997 100644 --- a/Controllers/SlidesController.cs +++ b/Controllers/SlidesController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NoticeBoard.Data; @@ -5,6 +6,7 @@ using NoticeBoard.Models; namespace NoticeBoard.Controllers; +[Authorize] public class SlidesController : Controller { private readonly AppDbContext _db; diff --git a/Program.cs b/Program.cs index 92d7475..374c03c 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication.Cookies; using NoticeBoard.Data; var builder = WebApplication.CreateBuilder(args); @@ -11,9 +12,19 @@ builder.Services.AddDbContext(options => builder.Services.AddControllersWithViews(); builder.Services.AddHttpClient(); +// Cookie authentication for admin panel +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.LoginPath = "/account/login"; + options.LogoutPath = "/account/logout"; + options.ExpireTimeSpan = TimeSpan.FromHours(12); + options.SlidingExpiration = true; + }); + var app = builder.Build(); -// Auto-create database on startup (use Migrate() if using EF migrations) +// Auto-create database on startup using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); @@ -28,12 +39,19 @@ if (!app.Environment.IsDevelopment()) app.UseStaticFiles(); app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); // Ensure uploads directory exists var uploadsPath = Path.Combine(app.Environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); +app.MapControllerRoute( + name: "account", + pattern: "account/{action=Login}", + defaults: new { controller = "Account" }); + app.MapControllerRoute( name: "admin", pattern: "admin/{action=Index}/{id?}", @@ -54,20 +72,17 @@ app.MapControllerRoute( pattern: "api/{action}/{id?}", defaults: new { controller = "Api" }); -// Display route: /{slug} — must be last to act as catch-all app.MapControllerRoute( name: "display", pattern: "d/{slug}", defaults: new { controller = "Display", action = "Show" }); -// Also support root-level slugs app.MapControllerRoute( name: "display-root", pattern: "{slug}", defaults: new { controller = "Display", action = "Show" }, constraints: new { slug = new NoticeBoard.Routing.DeviceSlugConstraint() }); -// Default route goes to admin app.MapControllerRoute( name: "default", pattern: "", diff --git a/Views/Account/Login.cshtml b/Views/Account/Login.cshtml new file mode 100644 index 0000000..d1f7b4e --- /dev/null +++ b/Views/Account/Login.cshtml @@ -0,0 +1,45 @@ +@{ + Layout = null; +} + + + + + + Sign In — Scratching Post + + + + + + + + diff --git a/appsettings.json b/appsettings.json index 2b99454..f2147b9 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,9 @@ } }, "AllowedHosts": "*", - "MaxUploadSizeMB": 20 + "MaxUploadSizeMB": 20, + "Admin": { + "Username": "admin", + "Password": "ScratchingPost2026!" + } }