Implemented user settings view

This commit is contained in:
2025-11-23 13:57:02 +01:00
parent 1c311d6e31
commit c554af9479
8 changed files with 286 additions and 18 deletions

View File

@@ -3,11 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using Novell.Directory.Ldap;
using Berufsschule_HAM.Models;
using Berufsschule_HAM.Helpers;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using System.Text.Json;
using System.Buffers.Text;
[Authorize]
[Route("[controller]")]
@@ -23,11 +19,60 @@ public class SettingsController : Controller
}
[HttpGet("User")]
public new IActionResult User()
public async Task<IActionResult> UserAsync()
{
return View();
try
{
var userID = User.Identity?.Name ?? throw new Exception("No name specified");
UserSettingsModel userSettingsModel = new()
{
userModel = await _ldap.GetUserByUidAsync(userID)
};
return View(userSettingsModel);
} catch (Exception ex)
{
_logger.LogWarning("An exception happened when trying to show user settings view: {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]);
return Redirect("/Home/Logout");
}
}
[HttpPut("User")]
public async Task<UserUpdateResponseModel> UpdateUserAsync([FromBody]UserUpdateRequestModel userUpdateRequestModel)
{
if (userUpdateRequestModel is null)
{
return new() {Success = false, ErrorMessage = "userUpdateRequestModel is null"};
}
try
{
var userID = User.Identity?.Name ?? throw new Exception("No name specified");
bool anyUpdated = false;
if (userUpdateRequestModel.Password is not null && userUpdateRequestModel.Password.Length > 0)
{
await _ldap.UpdateUser(userID, "userPassword", await UsersHelper.HashPassword(_ldap, userUpdateRequestModel.Password));
anyUpdated = true;
}
if (userUpdateRequestModel.Image is not null && userUpdateRequestModel.Image.Length > 0)
{
await _ldap.UpdateUser(userID, "jpegPhoto", userUpdateRequestModel.Image);
anyUpdated = true;
}
if (anyUpdated)
{
return new() {Success = true};
}
else
{
return new() {Success = false, ErrorMessage = "Nothing was updated"};
}
} catch (Exception ex)
{
_logger.LogWarning("An exception happened when trying to update a user: {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]);
return new() {Success = false, ErrorMessage = ex.Message};
}
}
[Authorize(Roles = "CanManageSettings")]
[HttpGet("Admin")]
public async Task<IActionResult> AdminAsync()

View File

@@ -70,7 +70,6 @@ public class UsersController : Controller
}
try
{
Task<AdminSettingsModel> settingsTask = _ldap.GetAdminSettingsModelAsync();
string? jpegPhoto = requestModel.JpegPhoto;
string? title = requestModel.Title;
string userPassword = requestModel.UserPassword ?? "";
@@ -81,10 +80,7 @@ public class UsersController : Controller
description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []};
if (!userPassword.StartsWith('{'))
{
AdminSettingsModel settings = await settingsTask;
byte[] passwordBytes = Encoding.UTF8.GetBytes(userPassword);
byte[] hashedPassword = settings.hashAlgorithm?.ComputeHash(passwordBytes) ?? throw new Exception("Hash algorithm not instantiated yet");
userPassword = $"{{{settings.DefaultHashAlgorithm.ToUpperInvariant()}}}{Convert.ToBase64String(hashedPassword)}";
userPassword = await UsersHelper.HashPassword(_ldap, userPassword);
}
LdapAttributeSet attributeSet =
@@ -118,7 +114,6 @@ public class UsersController : Controller
}
try
{
Task<AdminSettingsModel> settingsTask = _ldap.GetAdminSettingsModelAsync();
string uid = requestModel.Uid;
UserModel? user = null;
if (requestModel.NewUid is not null && requestModel.NewUid.Length > 0)
@@ -140,11 +135,8 @@ public class UsersController : Controller
await _ldap.UpdateUser(uid, "jpegPhoto", requestModel.JpegPhoto);
}
if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0)
{
AdminSettingsModel settings = await settingsTask;
byte[] passwordBytes = Encoding.UTF8.GetBytes(requestModel.UserPassword);
byte[] hashedPassword = settings.hashAlgorithm?.ComputeHash(passwordBytes) ?? throw new Exception("Hash algorithm not instantiated yet");
requestModel.UserPassword = $"{{{settings.DefaultHashAlgorithm.ToUpperInvariant()}}}{Convert.ToBase64String(hashedPassword)}";
{
requestModel.UserPassword = await UsersHelper.HashPassword(_ldap, requestModel.UserPassword);
await _ldap.UpdateUser(uid, "userPassword", requestModel.UserPassword);
}

View File

@@ -1,6 +1,8 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Berufsschule_HAM.Models;
using Berufsschule_HAM.Services;
namespace Berufsschule_HAM.Helpers;
public static partial class UsersHelper
@@ -39,6 +41,14 @@ public static partial class UsersHelper
return cleaned;
}
public static async Task<string> HashPassword(LdapService ldapService, string password)
{
AdminSettingsModel settings = await ldapService.GetAdminSettingsModelAsync();
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] hashedPassword = settings.hashAlgorithm?.ComputeHash(passwordBytes) ?? throw new Exception("Hash algorithm not instantiated yet");
return $"{{{settings.DefaultHashAlgorithm.ToUpperInvariant()}}}{Convert.ToBase64String(hashedPassword)}";
}
[GeneratedRegex("[^a-z]")]
private static partial Regex AtoZ();
}

View File

@@ -2,6 +2,15 @@ using System.Text.Json.Serialization;
namespace Berufsschule_HAM.Models;
public class UserUpdateRequestModel
{
[JsonPropertyName("Password")]
public string? Password { get; set; }
[JsonPropertyName("Image")]
public string? Image { get; set; }
}
public class AdminUpdateRequestModel
{
[JsonPropertyName("AdminSettingsModel")]

View File

@@ -2,6 +2,14 @@ using System.Text.Json.Serialization;
namespace Berufsschule_HAM.Models;
public class UserUpdateResponseModel
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
[JsonPropertyName("ErrorMessage")]
public string? ErrorMessage { get; set; }
}
public class AdminUpdateResponseModel
{
[JsonPropertyName("Success")]

View File

@@ -0,0 +1,6 @@
using Berufsschule_HAM.Models;
public class UserSettingsModel
{
public required UserModel userModel;
}

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value>
</resheader>
<data name="User settings" xml:space="preserve">
<value>Einstellungen</value>
</data>
<data name="Personal data" xml:space="preserve">
<value>Persönliche Daten</value>
</data>
<data name="Username" xml:space="preserve">
<value>Benutzername</value>
</data>
<data name="Title" xml:space="preserve">
<value>Anrede</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Surname" xml:space="preserve">
<value>Nachname</value>
</data>
<data name="Birth date" xml:space="preserve">
<value>Geburtsdatum</value>
</data>
<data name="Address" xml:space="preserve">
<value>Adresse</value>
</data>
<data name="Account" xml:space="preserve">
<value>Konto</value>
</data>
<data name="Workplace" xml:space="preserve">
<value>Arbeitsplatz</value>
</data>
<data name="Password" xml:space="preserve">
<value>Passwort</value>
</data>
<data name="Photo" xml:space="preserve">
<value>Profilbild</value>
</data>
<data name="Apply settings" xml:space="preserve">
<value>Änderungen anwenden</value>
</data>
<data name="Password must be at least 8 characters long and include upper, lower, number, and special character" xml:space="preserve">
<value>Passwort muss mindestens 8 Zeichen lang sein und Groß- und Kleinbuchstaben, sowie Zahlen und Sonderzeichen enthalten</value>
</data>
<data name="Settings updated successfully" xml:space="preserve">
<value>Einstellungen erfolgreich aktualisiert</value>
</data>
<data name="Unknown error" xml:space="preserve">
<value>Unbekannter Fehler</value>
</data>
<data name="Error contacting server" xml:space="preserve">
<value>Fehler bei der Kommunikation mit dem Server</value>
</data>
</root>

View File

@@ -1 +1,132 @@
<h3>Empty view lol</h3>
@using Microsoft.AspNetCore.Mvc.Localization
@using Berufsschule_HAM.Models
@model UserSettingsModel
@inject IViewLocalizer T
@inject IConfiguration Configuration
@{
ViewData["Title"] = T["User settings"];
string barcodeType = Configuration["BarcodeType"] ?? "EAN13";
}
<form id="updateSettings" method="post" asp-controller="Settings" asp-action="User">
<h4 class="fw-bold">@T["Account"]</h4>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label" for="username">@T["Username"]</label>
<input type="text" id="username" class="form-control" value="@Model.userModel.Uid" readonly/>
</div>
<div class="col-md-4">
<label class="form-label" for="password">@T["Password"]</label>
<input type="password" id="password" name="Password" class="form-control" />
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label" for="updatePhotoFile">@T["Photo"]</label>
<input type="file" id="updatePhotoFile" accept="image/*" class="form-control" />
<input type="hidden" id="updateJpegPhoto" name="Image" />
<div class="mt-2 text-center">
<img id="updatePhotoPreview" src="" alt="Preview" class="img-thumbnail" style="max-height: 150px; display: none;" />
</div>
</div>
</div>
<h4 class="fw-bold">@T["Personal data"]</h4>
<div class="row g-3 mb-3">
<div class="col-md-1">
<label class="form-label" for="title">@T["Title"]</label>
<input type="text" id="title" class="form-control" value="@Model.userModel.Title" readonly/>
</div>
<div class="col-md-4">
<label class="form-label" for="name">@T["Name"]</label>
<input type="text" id="name" class="form-control" value="@Model.userModel.Cn" readonly/>
</div>
<div class="col-md-4">
<label class="form-label" for="surname">@T["Surname"]</label>
<input type="text" id="surname" class="form-control" value="@Model.userModel.Sn" readonly/>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-2">
<label class="form-label" for="birthdate">@T["Birth date"]</label>
<input type="text" id="birthdate" class="form-control" value="@Model.userModel.Description?.BirthDate" readonly/>
</div>
<div class="col-md-4">
<label class="form-label" for="address">@T["Address"]</label>
<input type="text" id="address" class="form-control" value="@Model.userModel.Description?.BirthDate" readonly/>
</div>
<div class="col-md-3">
<label class="form-label" for="workplace">@T["Workplace"]</label>
<input type="text" id="workplace" class="form-control" value="@Model.userModel.Description?.Workplace" readonly/>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-12">
<button type="submit" class="btn btn-warning float-end mt-3">@T["Apply settings"]</button>
</div>
</div>
</form>
<script defer>
document.addEventListener('DOMContentLoaded', () => {
const photoInput = document.getElementById('updatePhotoFile');
const photoPreview = document.getElementById('updatePhotoPreview');
const photoHidden = document.getElementById('updateJpegPhoto');
const updateForm = document.getElementById('updateSettings');
updateForm.addEventListener('submit', async e => {
e.preventDefault();
const password = updateForm.querySelector('#password').value;
if (password != null && password.length > 0 && !validatePassword(password)) {
showToast('@T["Password must be at least 8 characters long and include upper, lower, number, and special character"]', 'danger');
return;
}
const url = `/Settings/User`;
const dataFromEntries = Object.fromEntries(new FormData(updateForm).entries());
var data = unflatten(dataFromEntries);
try {
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.Success) {
showToast('@T["Settings updated successfully"]', 'success');
let userDropdown = document.getElementById("userDropdown");
userDropdown.children[0].src = userDropdown.children[0].src + "&cachebuster=" + Date.now(); // Force refresh user image
photoHidden.value = "";
photoPreview.src = "";
photoPreview.style.display = 'none';
updateForm.reset();
} else {
showToast(`${result.reason}: ${result.exception || '@T["Unknown error"]'}`, 'danger');
}
} catch (error) {
console.error(error);
showToast('@T["Error contacting server"]', 'danger');
}
});
photoInput.addEventListener('change', () => {
const file = photoInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
const base64 = e.target.result.split(',')[1]; // strip data:image/jpeg;base64,
photoHidden.value = base64;
photoPreview.src = e.target.result;
photoPreview.style.display = 'block';
};
reader.readAsDataURL(file);
});
});
</script>