mirror of
https://github.com/LD-Reborn/Berufsschule_HAM.git
synced 2025-12-20 06:51:55 +00:00
Added UserModel, Added Login and Logout and authorization, Made dark mode default
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Berufsschule_HAM.Models;
|
||||
using Novell.Directory.Ldap;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using System.Security.Claims;
|
||||
using Berufsschule_HAM.Services;
|
||||
using ElmahCore;
|
||||
using Berufsschule_HAM.Exceptions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Route("[controller]")]
|
||||
@@ -16,11 +19,72 @@ public class HomeController : Controller
|
||||
_ldap = ldap ?? throw new ArgumentNullException(nameof(ldap));
|
||||
}
|
||||
|
||||
// GET: /Assets
|
||||
[Authorize]
|
||||
[HttpGet("Index")]
|
||||
[HttpGet("/")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost("Login")]
|
||||
public async Task<ActionResult> Login(string username, string password)
|
||||
{
|
||||
var authenticationResult = await _ldap.AuthenticateUser(username, password);
|
||||
if (authenticationResult.Success)
|
||||
{
|
||||
List<Claim> claims =
|
||||
[
|
||||
new(ClaimTypes.Name, username)
|
||||
];
|
||||
|
||||
var claimsIdentity = new ClaimsIdentity(
|
||||
claims,
|
||||
CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
var authProperties = new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = true,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(300)
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(claimsIdentity),
|
||||
authProperties);
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
switch (authenticationResult.AuthenticationState)
|
||||
{
|
||||
case UserNotAuthenticatedReason.InvalidCredentials:
|
||||
return View(new LoginViewModel() { ErrorText = "Invalid login credentials" }); // TODO add localization (e.g. T["Invalid login credentials"]; see: https://learn.microsoft.com/de-de/dotnet/core/extensions/localization)
|
||||
case UserNotAuthenticatedReason.UserLockedOut:
|
||||
return View(new LoginViewModel() { ErrorText = "Your account has been locked. Wait a few minutes or ask an administrator to unlock you" }); // TODO add localization (e.g. T["Invalid login credentials"]; see: https://learn.microsoft.com/de-de/dotnet/core/extensions/localization)
|
||||
case UserNotAuthenticatedReason.UserNotAuthorized:
|
||||
return View(new LoginViewModel() { ErrorText = "You are not authorized for login. Ask an administrator to authorize you." }); // TODO add localization (e.g. T["Invalid login credentials"]; see: https://learn.microsoft.com/de-de/dotnet/core/extensions/localization)
|
||||
default:
|
||||
await HttpContext.RaiseError(new HellFrozeOverException());
|
||||
return View(new LoginViewModel() { ErrorText = "Hell froze over. Make a screenshot and send it to an administrator." }); // TODO add localization (e.g. T["Invalid login credentials"]; see: https://learn.microsoft.com/de-de/dotnet/core/extensions/localization)
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("Login")]
|
||||
public ActionResult Login()
|
||||
{
|
||||
return View(new LoginViewModel());
|
||||
}
|
||||
|
||||
[HttpGet("Logout")]
|
||||
public ActionResult Logout()
|
||||
{
|
||||
HttpContext.SignOutAsync();
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
[HttpGet("AccessDenied")]
|
||||
public ActionResult AccessDenied()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public class UsersController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("Index")]
|
||||
public async Task<IEnumerable<Dictionary<string, string>>> Index(UsersIndexRequestModel requestModel)
|
||||
public async Task<IEnumerable<UserModel>> Index(UsersIndexRequestModel requestModel)
|
||||
{
|
||||
string? uid = requestModel.Uid;
|
||||
List<string> attributes = ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"];
|
||||
@@ -96,12 +96,12 @@ public class UsersController : Controller
|
||||
return false;
|
||||
}
|
||||
string uid = requestModel.uid;
|
||||
Dictionary<string, string>? user = null;
|
||||
UserModel? user = null;
|
||||
if (requestModel.Cn is not null)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "cn", requestModel.Cn);
|
||||
user ??= await _ldap.GetUserByUidAsync(uid);
|
||||
string newUid = user["sn"].ToLower() + requestModel.Cn.ToLower();
|
||||
string newUid = user.Sn?.ToLower() + requestModel.Cn.ToLower();
|
||||
await _ldap.UpdateUser(uid, "uid", newUid);
|
||||
uid = newUid;
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public class UsersController : Controller
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "sn", requestModel.Sn);
|
||||
user ??= await _ldap.GetUserByUidAsync(uid);
|
||||
string newUid = requestModel.Sn.ToLower() + user["cn"].ToLower();
|
||||
string newUid = requestModel.Sn.ToLower() + user.Cn?.ToLower();
|
||||
await _ldap.UpdateUser(uid, "uid", newUid);
|
||||
uid = newUid;
|
||||
}
|
||||
|
||||
3
src/Exceptions/HellFrozeOverException.cs
Normal file
3
src/Exceptions/HellFrozeOverException.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Berufsschule_HAM.Exceptions;
|
||||
|
||||
public class HellFrozeOverException : Exception {}
|
||||
6
src/Models/LoginViewModel.cs
Normal file
6
src/Models/LoginViewModel.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Berufsschule_HAM.Models;
|
||||
|
||||
class LoginViewModel
|
||||
{
|
||||
public string? ErrorText { get; set; }
|
||||
}
|
||||
36
src/Models/UserModels.cs
Normal file
36
src/Models/UserModels.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace Berufsschule_HAM.Models;
|
||||
|
||||
public class UserModel
|
||||
{
|
||||
public required string Uid { get; set; }
|
||||
public string? Cn { get; set; }
|
||||
public string? Sn { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? JpegPhoto { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? UserPassword { get; set; }
|
||||
public UserModel(Dictionary<string, string> ldapData)
|
||||
{
|
||||
Uid = ldapData.GetValueOrDefault("uid") ?? throw new ArgumentException("UID is required");
|
||||
Cn = ldapData.GetValueOrDefault("cn");
|
||||
Sn = ldapData.GetValueOrDefault("sn");
|
||||
Description = ldapData.GetValueOrDefault("description");
|
||||
JpegPhoto = ldapData.GetValueOrDefault("jpegPhoto");
|
||||
Title = ldapData.GetValueOrDefault("title");
|
||||
UserPassword = ldapData.GetValueOrDefault("userPassword");
|
||||
}
|
||||
}
|
||||
|
||||
public class UserAuthenticationResult
|
||||
{
|
||||
public required bool Success;
|
||||
public UserNotAuthenticatedReason AuthenticationState { get; set; } = UserNotAuthenticatedReason.None;
|
||||
}
|
||||
|
||||
public enum UserNotAuthenticatedReason
|
||||
{
|
||||
None,
|
||||
InvalidCredentials,
|
||||
UserNotAuthorized,
|
||||
UserLockedOut
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using ElmahCore;
|
||||
using ElmahCore.Mvc;
|
||||
using Serilog;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Berufsschule_HAM.Services;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -26,6 +27,14 @@ builder.Services.AddElmah<XmlFileErrorLog>(Options =>
|
||||
|
||||
builder.Services.AddSingleton<LdapService>();
|
||||
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/Home/Login";
|
||||
options.LogoutPath = "/Home/Login/Logout";
|
||||
options.AccessDeniedPath = "/Home/AccessDenied";
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using Novell.Directory.Ldap;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Berufsschule_HAM.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Berufsschule_HAM.Services;
|
||||
public class LdapService : IDisposable
|
||||
public partial class LdapService : IDisposable
|
||||
{
|
||||
private readonly LdapOptions _opts;
|
||||
private readonly LdapConnection _conn;
|
||||
@@ -49,19 +53,30 @@ public class LdapService : IDisposable
|
||||
return await ListObjectBy(UsersBaseDn, "", ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"]);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dictionary<string, string>>> ListUsersAsync(string[] attributes)
|
||||
public async Task<IEnumerable<UserModel>> ListUsersAsync(string[] attributes)
|
||||
{
|
||||
return await ListObjectBy(UsersBaseDn, "", attributes);
|
||||
List<UserModel> returnValue = [];
|
||||
(await ListObjectBy(UsersBaseDn, "", attributes))
|
||||
.ToList()
|
||||
.ForEach(x =>
|
||||
returnValue.Add(
|
||||
new UserModel(x)
|
||||
{
|
||||
Uid = x["uid"]
|
||||
}
|
||||
)
|
||||
);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> GetUserByUidAsync(string uid)
|
||||
public async Task<UserModel> GetUserByUidAsync(string uid)
|
||||
{
|
||||
return (await ListObjectBy(UsersBaseDn, $"uid={uid}", ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"])).First();
|
||||
return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"])).First()) {Uid = uid};
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> GetUserByUidAsync(string uid, string[] attributes)
|
||||
public async Task<UserModel> GetUserByUidAsync(string uid, string[] attributes)
|
||||
{
|
||||
return (await ListObjectBy(UsersBaseDn, $"uid={uid}", attributes)).First();
|
||||
return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", attributes)).First()) {Uid = uid};
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +101,46 @@ public class LdapService : IDisposable
|
||||
CreateObject(LocationsBaseDn, attributeSet);
|
||||
}
|
||||
|
||||
public async Task<UserAuthenticationResult> AuthenticateUser(string username, string password)
|
||||
{
|
||||
await ConnectAndBind();
|
||||
try
|
||||
{
|
||||
UserModel user = await GetUserByUidAsync(username);
|
||||
if (user.UserPassword is null)
|
||||
{
|
||||
return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
|
||||
}
|
||||
if (CompareStringToSha256(password, user.UserPassword))
|
||||
{
|
||||
return new() { Success = true};
|
||||
}
|
||||
return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
|
||||
}
|
||||
catch (LdapException)
|
||||
{
|
||||
return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
|
||||
}
|
||||
}
|
||||
|
||||
public bool CompareStringToSha256(string sourcePassword, string targetPasswordHashed)
|
||||
{
|
||||
byte[] sourcePasswordBytes = SHA256.HashData(Encoding.UTF8.GetBytes(sourcePassword));
|
||||
byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, ""));
|
||||
if (sourcePasswordBytes.Length != targetPasswordHashedBytes.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < sourcePasswordBytes.Length; i++)
|
||||
{
|
||||
if (sourcePasswordBytes[i] != targetPasswordHashedBytes[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private string PrependRDN(string rdn, string dn)
|
||||
{
|
||||
return rdn + "," + dn;
|
||||
@@ -166,4 +221,7 @@ public class LdapService : IDisposable
|
||||
_conn.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\{.*?\}")]
|
||||
private static partial Regex CurlyBracesRemover();
|
||||
}
|
||||
9
src/Views/Home/AccessDenied.cshtml
Normal file
9
src/Views/Home/AccessDenied.cshtml
Normal file
@@ -0,0 +1,9 @@
|
||||
@{
|
||||
ViewData["Title"] = "Access denied";
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Access denied</h1>
|
||||
<p>You currently do not have permission to access the page you tried to access.</p>
|
||||
<a href="/Home/Login">Please click here to return to the login page</a>
|
||||
</div>
|
||||
28
src/Views/Home/Login.cshtml
Normal file
28
src/Views/Home/Login.cshtml
Normal file
@@ -0,0 +1,28 @@
|
||||
@{
|
||||
ViewData["Title"] = "Login";
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Login</h1>
|
||||
@{
|
||||
if (Model.ErrorText is not null)
|
||||
{
|
||||
<div class="alert alert-danger text-center login-error">
|
||||
<h2>@Model.ErrorText</h2>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<form method="post" action="/Home/Login" class="mt-4" style="max-width: 400px; margin: auto;">
|
||||
<div class="form-group mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -9,9 +9,9 @@
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/Berufsschule_HAM.styles.css" asp-append-version="true" />
|
||||
</head>
|
||||
<body>
|
||||
<body data-bs-theme="dark">
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
|
||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar bg border-bottom box-shadow mb-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Berufsschule_HAM</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
@@ -21,10 +21,17 @@
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
|
||||
<a class="nav-link text" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||
@if (User.Identity.IsAuthenticated)
|
||||
{
|
||||
<a class="nav-link text" asp-area="" asp-controller="Home" asp-action="Logout">Log out</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="nav-link text" asp-area="" asp-controller="Home" asp-action="Login">Login</a>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -39,7 +46,7 @@
|
||||
|
||||
<footer class="border-top footer text-muted">
|
||||
<div class="container">
|
||||
© 2025 - Berufsschule_HAM - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||
© 2025 - Berufsschule_HAM
|
||||
</div>
|
||||
</footer>
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
@@ -28,4 +28,10 @@ body {
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
display: inline-block;
|
||||
margin: 1rem 0 1rem 0;
|
||||
padding: 2rem 4rem 2rem 4rem;
|
||||
}
|
||||
Reference in New Issue
Block a user