diff --git a/docs/Specs/20250928 Object attribute specs.md b/docs/Specs/Object attribute specs.md similarity index 88% rename from docs/Specs/20250928 Object attribute specs.md rename to docs/Specs/Object attribute specs.md index 5b10040..71ab039 100644 --- a/docs/Specs/20250928 Object attribute specs.md +++ b/docs/Specs/Object attribute specs.md @@ -14,14 +14,11 @@ ## locations - ObjectClass: - - extensibleObject - locality - top -- cn = location identifier (e.g. location name + room name + seat) -- l = location name (e.g. "btg") -- street = Street name and number (e.g. "Overwegstraße 30") +- l = location identifier (e.g. location name + room name + seat) - description = json string containing data as JSON. E.g.: - - `{"RoomNumber": "317", "Seat": "23"}` + - `{"Location": "BTG", "RoomNumber": "317", "Seat": "23"}` ## groups - ObjectClass: diff --git a/src/Controllers/LocationsController.cs b/src/Controllers/LocationsController.cs index 1833342..d8bb9dc 100644 --- a/src/Controllers/LocationsController.cs +++ b/src/Controllers/LocationsController.cs @@ -3,6 +3,8 @@ using Berufsschule_HAM.Models; using Microsoft.AspNetCore.Mvc; using System.Text.Json; using Microsoft.AspNetCore.Authorization; +using Novell.Directory.Ldap; +using Berufsschule_HAM.Helpers; [Authorize] [Route("[controller]")] @@ -24,6 +26,31 @@ public class LocationsController : Controller return list; } + [HttpGet("Create")] + public async Task Create(LocationsCreateRequestModel model) + { + try + { + LocationsDescription room = model.LocationsDescription; + string location = StringHelpers.Slugify(room.Location + " " + room.RoomNumber + " " + room.Seat); + LdapAttributeSet attributeSet = + [ + new LdapAttribute("objectClass", "locality"), + new LdapAttribute("objectClass", "top"), + new LdapAttribute("l", location), + new LdapAttribute("description", JsonSerializer.Serialize(room)) + ]; + + await _ldap.CreateLocation(attributeSet); + return true; + } + catch (Exception ex) + { + _logger.LogError("Unable to create location: {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]); + return false; + } + } + [HttpGet("Delete")] public async Task Delete(string cn) { @@ -49,37 +76,20 @@ public class LocationsController : Controller _logger.LogError("Unable to update a location because the LocationsModifyRequestModel is null"); return false; } - string cn = requestModel.Cn; + try + { + string location = requestModel.Location; + LocationsDescription room = requestModel.Description; + string newLocation = StringHelpers.Slugify(room.Location + " " + room.RoomNumber + " " + room.Seat); // TODO: fix DRY violation - if (requestModel.NewCn is not null) - { - await _ldap.UpdateLocation(cn, "cn", requestModel.NewCn); - cn = requestModel.NewCn; + await _ldap.UpdateLocation(location, "description", JsonSerializer.Serialize(room)); + await _ldap.UpdateLocation(location, "l", newLocation); + return true; } - if (requestModel.Location is not null) + catch (Exception ex) { - await _ldap.UpdateLocation(cn, "location", requestModel.Location); + _logger.LogError("Unable to update location: {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]); + return false; } - if (requestModel.Street is not null) - { - await _ldap.UpdateLocation(cn, "street", requestModel.Street); - } - if (requestModel.Description is not null) - { - LocationsDescription description = requestModel.Description; - LocationModel? location = null; - if (description.Seat is null) - { - location ??= await _ldap.GetLocationByCnAsync(cn); - description.Seat = location.Description?.Seat; - } - else if (description.RoomNumber is null) - { - location ??= await _ldap.GetLocationByCnAsync(cn); - description.RoomNumber = location.Description?.RoomNumber; - } - await _ldap.UpdateLocation(cn, "description", JsonSerializer.Serialize(requestModel.Description)); - } - return true; } } diff --git a/src/Helpers/StringHelper.cs b/src/Helpers/StringHelper.cs new file mode 100644 index 0000000..f65ce3b --- /dev/null +++ b/src/Helpers/StringHelper.cs @@ -0,0 +1,19 @@ +using System.Text.RegularExpressions; + +namespace Berufsschule_HAM.Helpers; + +public static class StringHelpers +{ + public static string Slugify(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return string.Empty; + } + input = input.ToLowerInvariant(); + input = Regex.Replace(input, @"[^a-z0-9\s-]", "", RegexOptions.None, TimeSpan.FromMilliseconds(100)); + input = Regex.Replace(input, @"[\s-]+", "-", RegexOptions.None, TimeSpan.FromMilliseconds(100)); + input = input.Trim('-'); + return input; + } +} \ No newline at end of file diff --git a/src/Models/LocationsModels.cs b/src/Models/LocationsModels.cs index 844d65e..bb6766e 100644 --- a/src/Models/LocationsModels.cs +++ b/src/Models/LocationsModels.cs @@ -4,15 +4,11 @@ using System.Text.Json; using Berufsschule_HAM.Exceptions; public class LocationModel { - public required string Cn { get; set; } + public required string Location { get; set; } public LocationsDescription? Description { get; set; } - public string? Location { get; set; } - public string? Street { get; set; } public LocationModel(Dictionary ldapData) { - Cn = ldapData.GetValueOrDefault("cn") ?? throw new LocationModelConfigurationException(); - Location = ldapData.GetValueOrDefault("l"); - Street = ldapData.GetValueOrDefault("street"); + Location = ldapData.GetValueOrDefault("l") ?? throw new LocationModelConfigurationException(); string? descriptionValue = ldapData.GetValueOrDefault("description"); if (descriptionValue is null) { @@ -26,6 +22,7 @@ public class LocationModel } public class LocationsDescription { + public string? Location { get; set; } public string? RoomNumber { get; set; } public string? Seat { get; set; } } \ No newline at end of file diff --git a/src/Models/LocationsRequestModels.cs b/src/Models/LocationsRequestModels.cs index 0c9c76f..1056783 100644 --- a/src/Models/LocationsRequestModels.cs +++ b/src/Models/LocationsRequestModels.cs @@ -1,10 +1,12 @@ namespace Berufsschule_HAM.Models; +public class LocationsCreateRequestModel +{ + public required LocationsDescription LocationsDescription { get; set; } +} + public class LocationsModifyRequestModel { - public required string Cn { get; set; } - public string? NewCn { get; set; } = null; - public LocationsDescription? Description { get; set; } = null; - public string? Location { get; set; } = null; - public string? Street { get; set; } = null; + public required string Location { get; set; } + public required LocationsDescription Description { get; set; } } \ No newline at end of file diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index 1e6c101..3d981bf 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -47,13 +47,13 @@ public partial class LdapService : IDisposable public string MigrationsBaseDn => string.IsNullOrEmpty(_opts.MigrationsOu) ? _opts.BaseDn : $"{_opts.MigrationsOu},{_opts.BaseDn}"; public string[] UsersAttributes => ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"]; public string[] AssetsAttributes => ["CN", "description", "l", "owner", "serialNumber", "name"]; - public string[] LocationsAttributes => ["cn", "l", "street", "description"]; + public string[] LocationsAttributes => ["l", "description"]; public string[] GroupsAttributes => ["cn", "gidNumber", "description"]; public async Task> ListLocationsAsync() { IEnumerable> locations = await ListObjectBy(LocationsBaseDn, "", LocationsAttributes); List models = []; - locations.ToList().ForEach(x => models.Add(new LocationModel(x) {Cn = x["cn"]})); + locations.ToList().ForEach(x => models.Add(new LocationModel(x) {Location = x[LocationsAttributes[0]]})); return models; } @@ -150,9 +150,9 @@ public partial class LdapService : IDisposable { return await GetLocationByCnAsync(cn, LocationsAttributes); } - public async Task GetLocationByCnAsync(string cn, string[] attributes) + public async Task GetLocationByCnAsync(string location, string[] attributes) { - return new LocationModel((await ListObjectBy(LocationsBaseDn, $"cn={cn}", attributes)).First()) { Cn = cn }; + return new LocationModel((await ListObjectBy(LocationsBaseDn, $"l={location}", attributes)).First()) { Location = location }; } public async Task GetAssetByCnAsync(string cn) @@ -197,7 +197,13 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) public async Task CreateLocation(LdapAttributeSet attributeSet) { - await CreateObject(LocationsBaseDn, attributeSet); + string? cn = attributeSet.GetAttribute("l")?.StringValue; + + if (string.IsNullOrEmpty(cn)) + throw new ArgumentException("AttributeSet must contain an l attribute."); + + string dn = PrependRDN($"l={cn}", LocationsBaseDn); + await CreateObject(dn, attributeSet); } public async Task AuthenticateUser(string username, string password) @@ -296,9 +302,9 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) await DeleteObjectByDnAsync(dn); } - public async Task DeleteLocationAsync(string cn) + public async Task DeleteLocationAsync(string location) { - string dn = PrependRDN($"cn={cn}", LocationsBaseDn); + string dn = PrependRDN($"l={location}", LocationsBaseDn); await DeleteObjectByDnAsync(dn); } @@ -318,9 +324,9 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) await UpdateObject(GroupsBaseDn, "cn", cn, attributeName, attributeValue); } - public async Task UpdateLocation(string cn, string attributeName, string attributeValue) + public async Task UpdateLocation(string location, string attributeName, string attributeValue) { - await UpdateObject(LocationsBaseDn, "cn", cn, attributeName, attributeValue); + await UpdateObject(LocationsBaseDn, "l", location, attributeName, attributeValue); } public async Task UpdateAsset(string cn, string attributeName, string attributeValue)