mirror of
https://github.com/LD-Reborn/Berufsschule_HAM.git
synced 2025-12-20 06:51:55 +00:00
Implemented user settings view
This commit is contained in:
@@ -3,11 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Novell.Directory.Ldap;
|
using Novell.Directory.Ldap;
|
||||||
using Berufsschule_HAM.Models;
|
using Berufsschule_HAM.Models;
|
||||||
using Berufsschule_HAM.Helpers;
|
using Berufsschule_HAM.Helpers;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Buffers.Text;
|
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
@@ -23,11 +19,60 @@ public class SettingsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("User")]
|
[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")]
|
[Authorize(Roles = "CanManageSettings")]
|
||||||
[HttpGet("Admin")]
|
[HttpGet("Admin")]
|
||||||
public async Task<IActionResult> AdminAsync()
|
public async Task<IActionResult> AdminAsync()
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ public class UsersController : Controller
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Task<AdminSettingsModel> settingsTask = _ldap.GetAdminSettingsModelAsync();
|
|
||||||
string? jpegPhoto = requestModel.JpegPhoto;
|
string? jpegPhoto = requestModel.JpegPhoto;
|
||||||
string? title = requestModel.Title;
|
string? title = requestModel.Title;
|
||||||
string userPassword = requestModel.UserPassword ?? "";
|
string userPassword = requestModel.UserPassword ?? "";
|
||||||
@@ -81,10 +80,7 @@ public class UsersController : Controller
|
|||||||
description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []};
|
description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []};
|
||||||
if (!userPassword.StartsWith('{'))
|
if (!userPassword.StartsWith('{'))
|
||||||
{
|
{
|
||||||
AdminSettingsModel settings = await settingsTask;
|
userPassword = await UsersHelper.HashPassword(_ldap, userPassword);
|
||||||
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)}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LdapAttributeSet attributeSet =
|
LdapAttributeSet attributeSet =
|
||||||
@@ -118,7 +114,6 @@ public class UsersController : Controller
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Task<AdminSettingsModel> settingsTask = _ldap.GetAdminSettingsModelAsync();
|
|
||||||
string uid = requestModel.Uid;
|
string uid = requestModel.Uid;
|
||||||
UserModel? user = null;
|
UserModel? user = null;
|
||||||
if (requestModel.NewUid is not null && requestModel.NewUid.Length > 0)
|
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);
|
await _ldap.UpdateUser(uid, "jpegPhoto", requestModel.JpegPhoto);
|
||||||
}
|
}
|
||||||
if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0)
|
if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0)
|
||||||
{
|
{
|
||||||
AdminSettingsModel settings = await settingsTask;
|
requestModel.UserPassword = await UsersHelper.HashPassword(_ldap, requestModel.UserPassword);
|
||||||
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)}";
|
|
||||||
await _ldap.UpdateUser(uid, "userPassword", requestModel.UserPassword);
|
await _ldap.UpdateUser(uid, "userPassword", requestModel.UserPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Berufsschule_HAM.Models;
|
||||||
|
using Berufsschule_HAM.Services;
|
||||||
namespace Berufsschule_HAM.Helpers;
|
namespace Berufsschule_HAM.Helpers;
|
||||||
|
|
||||||
public static partial class UsersHelper
|
public static partial class UsersHelper
|
||||||
@@ -39,6 +41,14 @@ public static partial class UsersHelper
|
|||||||
return cleaned;
|
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]")]
|
[GeneratedRegex("[^a-z]")]
|
||||||
private static partial Regex AtoZ();
|
private static partial Regex AtoZ();
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,15 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Berufsschule_HAM.Models;
|
namespace Berufsschule_HAM.Models;
|
||||||
|
|
||||||
|
public class UserUpdateRequestModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Password")]
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("Image")]
|
||||||
|
public string? Image { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class AdminUpdateRequestModel
|
public class AdminUpdateRequestModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("AdminSettingsModel")]
|
[JsonPropertyName("AdminSettingsModel")]
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Berufsschule_HAM.Models;
|
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
|
public class AdminUpdateResponseModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Success")]
|
[JsonPropertyName("Success")]
|
||||||
|
|||||||
6
src/Models/UserSettingsModel.cs
Normal file
6
src/Models/UserSettingsModel.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using Berufsschule_HAM.Models;
|
||||||
|
|
||||||
|
public class UserSettingsModel
|
||||||
|
{
|
||||||
|
public required UserModel userModel;
|
||||||
|
}
|
||||||
67
src/Resources/Views.Settings.User.de.resx
Normal file
67
src/Resources/Views.Settings.User.de.resx
Normal 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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user