Implemented user creation in frontend

This commit is contained in:
2025-10-25 22:44:36 +02:00
parent 310e05545f
commit 85cb68a6c2
4 changed files with 220 additions and 14 deletions

View File

@@ -60,15 +60,19 @@ public class UsersController : Controller
}
}
[HttpGet("Create")]
public async Task<bool> Create(string cn, string sn, string? title, string? uid, string userPassword, string? description, string jpegPhoto)
[HttpPost("Create")]
public async Task<UsersCreateResponseModel> 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 ??= UsersHelper.CreateUsername(cn, sn);
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,26 +83,26 @@ 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<UsersUpdateRequestModel> Update([FromBody] UsersModifyRequestModel requestModel)
public async Task<UsersUpdateResponseModel> Update([FromBody] UsersModifyRequestModel requestModel)
{
if (requestModel is null)
{

View File

@@ -13,12 +13,16 @@ public class UsersIndexRequestModel
public bool UserPassword { get; set; } = true;
}
public class UsersModifyRequestModel
public class UsersModifyRequestModel : UsersCreateRequestModel
{
[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")]

View File

@@ -2,7 +2,7 @@ using System.Text.Json.Serialization;
namespace Berufsschule_HAM.Models;
public class UsersUpdateRequestModel
public class UsersUpdateResponseModel
{
[JsonPropertyName("Success")]
public required bool Success { get; set; }
@@ -11,3 +11,14 @@ public class UsersUpdateRequestModel
[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; }
}

View File

@@ -15,7 +15,9 @@
<div class="mb-4 d-flex flex-wrap gap-2">
<button class="btn btn-primary">@T["Create user"]</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
@T["Create user"]
</button>
</div>
@@ -461,3 +463,188 @@
});
</script>
<!-- User Create Modal -->
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="createModalLabel">Create User</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createForm">
<div class="row g-3">
<h6 class="fw-bold">Personal information</h6>
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" name="Title" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">Name</label>
<input type="text" name="Cn" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">Surname</label>
<input type="text" name="Sn" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">Birth date</label>
<input type="text" name="Description.BirthDate" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">City</label>
<input type="text" name="Description.Address.City" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">Street</label>
<input type="text" name="Description.Address.Street" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">Street Nr.</label>
<input type="text" name="Description.Address.StreetNr" class="form-control" />
</div>
<hr class="my-3">
<h6 class="fw-bold">Workplace & account</h6>
<div class="col-md-6">
<label class="form-label">Workplace</label>
<input type="text" name="Description.Workplace" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">Groups</label>
<select id="createGroups" name="Description.Groups" class="form-select" multiple></select>
</div>
<div class="col-md-6">
<label class="form-label">Password</label>
<input type="password" name="UserPassword" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label">Photo</label>
<input type="file" id="createPhotoFile" accept="image/*" class="form-control" />
<input type="hidden" id="createJpegPhoto" name="JpegPhoto" />
<div class="mt-2 text-center">
<img id="createPhotoPreview" src="" alt="Preview" class="img-thumbnail" style="max-height: 150px; display: none;" />
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="createForm" class="btn btn-primary">Create</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const createModal = document.getElementById('createModal');
const createForm = document.getElementById('createForm');
const createGroupsSelect = document.getElementById('createGroups');
const photoInput = document.getElementById('createPhotoFile');
const photoPreview = document.getElementById('createPhotoPreview');
const photoHidden = document.getElementById('createJpegPhoto');
// Load available groups when modal is shown
createModal.addEventListener('show.bs.modal', async () => {
await populateGroupsDropdown(createGroupsSelect);
createForm.reset();
photoPreview.style.display = 'none';
});
// Handle photo upload preview
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];
photoHidden.value = base64;
photoPreview.src = e.target.result;
photoPreview.style.display = 'block';
};
reader.readAsDataURL(file);
});
// Submit create form
createForm.addEventListener('submit', async e => {
e.preventDefault();
const dataFromEntries = Object.fromEntries(new FormData(createForm).entries());
const data = unflatten(dataFromEntries);
data.Description.Groups = Array.from(createGroupsSelect.selectedOptions).map(o => o.value);
try {
const response = await fetch('/Users/Create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.Success) {
bootstrap.Modal.getInstance(createModal).hide();
showToast('User created successfully', 'success');
// Add new row to table dynamically
const tbody = document.querySelector('table tbody');
const newRow = document.createElement('tr');
newRow.innerHTML = `
<td><img class="rounded-circle user-icon" src="data:image/jpeg;base64,${data.JpegPhoto || ''}" alt="Photo" style="max-width:300px;" /></td>
<td>${result.NewUid || ''}</td>
<td>${data.Title || ''}</td>
<td>${data.Cn || ''}</td>
<td>${data.Sn || ''}</td>
<td>${data.Description?.Workplace || ''}</td>
<td>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-warning"
data-user-id="${result.Uid || ''}"
data-user-title="${data.Title || ''}"
data-user-name="${data.Cn || ''}"
data-user-surname="${data.Sn || ''}"
data-user-workplace="${data.Description?.Workplace || ''}"
data-user-groups='${JSON.stringify(data.Description?.Groups || [])}'
data-bs-toggle="modal"
data-bs-target="#updateModal">
Update
</button>
<button class="btn btn-sm btn-danger btn-delete"
data-user-id="${result.NewUid || ''}"
data-user-name="${data.Cn || ''}"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
Delete
</button>
</div>
</td>
`;
tbody.appendChild(newRow);
} else {
showToast(`${result.Exception || 'Create failed'}`, 'danger');
}
} catch (err) {
console.error(err);
showToast('Error creating user', 'danger');
}
});
async function populateGroupsDropdown(selectElement) {
try {
const res = await fetch('/Groups/Get');
const json = await res.json();
if (!json.success) return;
selectElement.innerHTML = '';
json.GroupModels.forEach(group => {
const opt = document.createElement('option');
opt.value = group.Cn;
opt.textContent = group.DisplayName || group.Cn;
selectElement.appendChild(opt);
});
} catch (err) {
console.error('Error fetching groups', err);
}
}
});
</script>