diff --git a/src/Controllers/HomeController.cs b/src/Controllers/HomeController.cs index dc276f7..9d5555a 100644 --- a/src/Controllers/HomeController.cs +++ b/src/Controllers/HomeController.cs @@ -89,7 +89,7 @@ public class HomeController : Controller Surname = user.Sn ?? "", Title = user.Title ?? "", Uid = user.Uid, - Workplace = user.Description?.Workplace ?? "" + Description = user.Description ?? new() {Address = new(), BirthDate = "", Workplace = "", Groups = []} }); } return View(new UsersIndexViewModel() { UserTableViewModels = UserTableViewModels }); diff --git a/src/Controllers/UsersController.cs b/src/Controllers/UsersController.cs index 96cb3b4..630c41b 100644 --- a/src/Controllers/UsersController.cs +++ b/src/Controllers/UsersController.cs @@ -1,4 +1,3 @@ -// Controllers/UsersController.cs using Berufsschule_HAM.Services; using Microsoft.AspNetCore.Mvc; using Novell.Directory.Ldap; @@ -8,6 +7,7 @@ using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Authorization; using System.Text.Json; +using System.Buffers.Text; [Authorize(Roles = "CanManageUsers")] [Route("[controller]")] @@ -60,15 +60,19 @@ public class UsersController : Controller } } - [HttpGet("Create")] - public async Task Create(string cn, string sn, string? title, string? uid, string userPassword, string? description, string jpegPhoto) + [HttpPost("Create")] + public async Task Create([FromBody] UsersCreateRequestModel requestModel) { try { + string? jpegPhoto = requestModel.JpegPhoto; + string? title = requestModel.Title; + string userPassword = requestModel.UserPassword ?? ""; + UserDescription? description = requestModel.Description; jpegPhoto ??= Convert.ToBase64String(System.IO.File.ReadAllBytes("wwwroot/user_default.jpeg")); // TODO: cleanup - make this a config setting - uid ??= sn.ToLower() + cn.ToLower(); + string uid = UsersHelper.CreateUsername(requestModel.Cn ?? "", requestModel.Sn ?? ""); title ??= ""; - description ??= "{}"; + description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []}; if (!userPassword.StartsWith('{')) { byte[] passwordBytes = Encoding.UTF8.GetBytes(userPassword); @@ -79,72 +83,86 @@ public class UsersController : Controller LdapAttributeSet attributeSet = [ new LdapAttribute("objectClass", "inetOrgPerson"), - new LdapAttribute("cn", cn), - new LdapAttribute("sn", sn), + new LdapAttribute("cn", requestModel.Cn), + new LdapAttribute("sn", requestModel.Sn), new LdapAttribute("title", title), new LdapAttribute("uid", uid), new LdapAttribute("jpegPhoto", jpegPhoto), - new LdapAttribute("description", description), + new LdapAttribute("description", JsonSerializer.Serialize(description)), new LdapAttribute("userPassword", userPassword), ]; await _ldap.CreateUser(uid, attributeSet); - return true; + return new(){Success = true, Uid = uid}; } catch (Exception ex) { _logger.LogError("Unable to create user: {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]); - return false; + return new() {Success = false, Exception = ex.Message}; } } [HttpPost("Update")] - public async Task Update([FromBody] UsersModifyRequestModel requestModel) + public async Task Update([FromBody] UsersModifyRequestModel requestModel) { if (requestModel is null) { _logger.LogError("Unable to update a user because the UsersModifyRequestModel is null"); - return false; + return new() { Success = false }; } - string uid = requestModel.uid; - UserModel? user = null; - if (requestModel.Cn is not null) + try { - await _ldap.UpdateUser(uid, "cn", requestModel.Cn); - user ??= await _ldap.GetUserByUidAsync(uid); - string newUid = user.Sn?.ToLower() + requestModel.Cn.ToLower(); - await _ldap.UpdateUser(uid, "uid", newUid); - uid = newUid; - } - if (requestModel.Sn is not null) + string uid = requestModel.Uid; + UserModel? user = null; + if (requestModel.NewUid is not null && requestModel.NewUid.Length > 0) + { + await _ldap.UpdateUser(uid, "uid", requestModel.NewUid); + uid = requestModel.NewUid; + } + if (requestModel.Title is not null) + { + await _ldap.UpdateUser(uid, "title", requestModel.Title); + } + if (requestModel.Description is not null) + { + await _ldap.UpdateUser(uid, "description", JsonSerializer.Serialize(requestModel.Description)); + } + if (requestModel.JpegPhoto is not null && requestModel.JpegPhoto.Length > 0) + { + await _ldap.UpdateUser(uid, "jpegPhoto", requestModel.JpegPhoto); + } + if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0) + { + await _ldap.UpdateUser(uid, "userPassword", "{SHA256}" + Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(requestModel.UserPassword)))); + } + + string newUid = uid; + if (requestModel.Cn is not null) + { + await _ldap.UpdateUser(uid, "cn", requestModel.Cn); + user ??= await _ldap.GetUserByUidAsync(uid); + newUid = UsersHelper.CreateUsername(requestModel.Cn, user.Sn ?? ""); + + } + if (requestModel.Sn is not null) + { + await _ldap.UpdateUser(uid, "sn", requestModel.Sn); + user ??= await _ldap.GetUserByUidAsync(uid); + newUid = UsersHelper.CreateUsername(user.Cn ?? "", requestModel.Sn); + } + if (newUid.Length == 0) + { + throw new Exception("Username cannot be empty"); + } + if (newUid != uid) + { + await _ldap.UpdateUser(uid, "uid", newUid); + uid = newUid; + } + return new() { Success = true, NewUid = uid }; + } catch (Exception ex) { - await _ldap.UpdateUser(uid, "sn", requestModel.Sn); - user ??= await _ldap.GetUserByUidAsync(uid); - string newUid = requestModel.Sn.ToLower() + user.Cn?.ToLower(); - await _ldap.UpdateUser(uid, "uid", newUid); - uid = newUid; + return new() { Success = false, Exception = ex.Message }; } - if (requestModel.NewUid is not null) - { - await _ldap.UpdateUser(uid, "uid", requestModel.NewUid); - uid = requestModel.NewUid; - } - if (requestModel.Title is not null) - { - await _ldap.UpdateUser(uid, "title", requestModel.Title); - } - if (requestModel.Description is not null) - { - await _ldap.UpdateUser(uid, "description", requestModel.Description); - } - if (requestModel.JpegPhoto is not null) - { - await _ldap.UpdateUser(uid, "jpegPhoto", requestModel.JpegPhoto); - } - if (requestModel.UserPassword is not null) - { - await _ldap.UpdateUser(uid, "userPassword", requestModel.UserPassword); - } - return true; } [HttpPost("AddGroup")] diff --git a/src/Helpers/UsersHelper.cs b/src/Helpers/UsersHelper.cs new file mode 100644 index 0000000..7f16c4e --- /dev/null +++ b/src/Helpers/UsersHelper.cs @@ -0,0 +1,44 @@ +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +namespace Berufsschule_HAM.Helpers; + +public static partial class UsersHelper +{ + public static string CreateUsername(string givenName, string surname) + { + if (string.IsNullOrWhiteSpace(givenName) || string.IsNullOrWhiteSpace(surname)) + throw new ArgumentException("Given name and surname must not be empty."); + + string combined = (surname + givenName).ToLowerInvariant(); + + // Normalize to decompose accents (e.g., é -> e + ́) + string normalized = combined.Normalize(NormalizationForm.FormD); + + // Remove diacritics + var sb = new StringBuilder(); + foreach (var c in normalized) + { + var category = CharUnicodeInfo.GetUnicodeCategory(c); + if (category != UnicodeCategory.NonSpacingMark) + sb.Append(c); + } + + string cleaned = sb.ToString(); + + // Replace German ligatures etc. + cleaned = cleaned + .Replace("ß", "ss") + .Replace("æ", "ae") + .Replace("œ", "oe") + .Replace("ø", "o"); + + // Remove everything not a-z + cleaned = AtoZ().Replace(cleaned, ""); + + return cleaned; + } + + [GeneratedRegex("[^a-z]")] + private static partial Regex AtoZ(); +} \ No newline at end of file diff --git a/src/Models/UserModels.cs b/src/Models/UserModels.cs index 7fe6616..52cd777 100644 --- a/src/Models/UserModels.cs +++ b/src/Models/UserModels.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; namespace Berufsschule_HAM.Models; @@ -26,17 +27,24 @@ public class UserModel public class UserDescription { + [JsonPropertyName("BirthDate")] public required string BirthDate { get; set; } + [JsonPropertyName("Address")] public required UserAddress Address { get; set; } + [JsonPropertyName("Workplace")] public required string Workplace { get; set; } + [JsonPropertyName("Groups")] public List? Groups { get; set; } } public class UserAddress { + [JsonPropertyName("City")] public string? City { get; set; } + [JsonPropertyName("Street")] public string? Street { get; set; } - public string? StreetNr { get; set; } + [JsonPropertyName("StreetNr")] + public int? StreetNr { get; set; } } public class UserAuthenticationResult @@ -61,5 +69,5 @@ public class UserTableViewModel public required string Title { get; set; } public required string Name { get; set; } public required string Surname { get; set; } - public required string Workplace { get; set; } + public required UserDescription Description { get; set; } } \ No newline at end of file diff --git a/src/Models/UsersRequestModels.cs b/src/Models/UsersRequestModels.cs index 0abfe5a..14361d7 100644 --- a/src/Models/UsersRequestModels.cs +++ b/src/Models/UsersRequestModels.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Berufsschule_HAM.Models; public class UsersIndexRequestModel @@ -11,15 +13,27 @@ public class UsersIndexRequestModel public bool UserPassword { get; set; } = true; } -public class UsersModifyRequestModel +public class UsersModifyRequestModel : UsersCreateRequestModel { - public required string uid { get; set; } + [JsonPropertyName("Uid")] + public required string Uid { get; set; } + [JsonPropertyName("NewUid")] public string? NewUid { get; set; } = null; +} + +public class UsersCreateRequestModel +{ + [JsonPropertyName("Cn")] public string? Cn { get; set; } = null; + [JsonPropertyName("Sn")] public string? Sn { get; set; } = null; + [JsonPropertyName("Title")] public string? Title { get; set; } = null; - public string? Description { get; set; } = null; + [JsonPropertyName("Description")] + public UserDescription? Description { get; set; } = null; + [JsonPropertyName("JpegPhoto")] public string? JpegPhoto { get; set; } = null; + [JsonPropertyName("UserPassword")] public string? UserPassword { get; set; } = null; } diff --git a/src/Models/UsersResponseModels.cs b/src/Models/UsersResponseModels.cs new file mode 100644 index 0000000..7e2a532 --- /dev/null +++ b/src/Models/UsersResponseModels.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Berufsschule_HAM.Models; + +public class UsersUpdateResponseModel +{ + [JsonPropertyName("Success")] + public required bool Success { get; set; } + [JsonPropertyName("Exception")] + public string? Exception { get; set; } + [JsonPropertyName("NewUid")] + public string? NewUid { get; set; } +} + + +public class UsersCreateResponseModel +{ + [JsonPropertyName("Success")] + public required bool Success { get; set; } + [JsonPropertyName("Exception")] + public string? Exception { get; set; } + [JsonPropertyName("Uid")] + public string? Uid { get; set; } +} \ No newline at end of file diff --git a/src/Resources/Views.Home.Users.de.resx b/src/Resources/Views.Home.Users.de.resx index d47f14b..a779788 100644 --- a/src/Resources/Views.Home.Users.de.resx +++ b/src/Resources/Views.Home.Users.de.resx @@ -19,4 +19,112 @@ Benutzer + + Benutzer erfolgreich gelöscht + + + Unbekannter Fehler + + + Server konnte nicht erreicht werden + + + Benutzer wurde erfolgreich angepasst + + + Anpassung ist fehlgeschlagen + + + Fehler beim Anpassen des Benutzers + + + Benutzer wurde erfolgreich erstellt + + + Erstellung des Benutzers ist fehlgeschlagen + + + Fehler beim Erstellen des Benutzers + + + Benutzername + + + Anrede + + + Name + + + Nachname + + + Arbeitsplatz + + + Aktion + + + Anpassen + + + Löschen + + + Sind Sie sich sicher, dass Sie den Benutzer {0} löschen möchten? + + + Ja, löschen + + + Abbrechen + + + Benutzer anpassen + + + Persönliche Informationen + + + Geburtsdatum + + + Stadt + + + Straße + + + Hausnummer + + + Arbeitsplatz und Benutzerkonto + + + Gruppen + + + Neues Passwort + + + Passwort + + + Foto + + + Änderungen anwenden + + + Anlegen + + + Benutzerdaten + + + Adresse + + + Löschen bestätigen + diff --git a/src/Resources/Views.Home.Users.en.resx b/src/Resources/Views.Home.Users.en.resx new file mode 100644 index 0000000..0a1a49b --- /dev/null +++ b/src/Resources/Views.Home.Users.en.resx @@ -0,0 +1,19 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + + + + Are you sure you want to delete user {0}? + + diff --git a/src/Views/Home/Users.cshtml b/src/Views/Home/Users.cshtml index c526b36..62ead14 100644 --- a/src/Views/Home/Users.cshtml +++ b/src/Views/Home/Users.cshtml @@ -1,6 +1,9 @@ +@using Microsoft.AspNetCore.Html @using Microsoft.AspNetCore.Mvc.Localization @using Berufsschule_HAM.Models @using System.Buffers.Text +@using System.Text.Json; +@using System.Text.Json.Serialization; @model UsersIndexViewModel @inject IViewLocalizer T @{ @@ -13,7 +16,9 @@
- +
@@ -22,19 +27,19 @@ - User ID - title - Name - Surname - Workplace - Action + @T["Username"] + @T["Title"] + @T["Name"] + @T["Surname"] + @T["Workplace"] + @T["Action"] @{ foreach (UserTableViewModel userTableViewModel in Model.UserTableViewModels) { - + Photo @@ -42,15 +47,29 @@ @userTableViewModel.Title @userTableViewModel.Name @userTableViewModel.Surname - @userTableViewModel.Workplace + @userTableViewModel.Description.Workplace
- +
@@ -67,16 +86,18 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file