297 lines
11 KiB
Plaintext
297 lines
11 KiB
Plaintext
@page "/admin"
|
|
@using Microsoft.AspNetCore.Authorization
|
|
@attribute [Authorize(Roles = "ROLE_ADMIN")]
|
|
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
|
@inject Fluxor.IDispatcher Dispatcher
|
|
@inject Fluxor.IState<Wollmilchsau.Store.AdminStore.AdminState> AdminState
|
|
@inject IJSRuntime JsRuntime
|
|
@inject ISnackbar Snackbar
|
|
|
|
<PageTitle>User Comparison</PageTitle>
|
|
|
|
<style>
|
|
.moodle-delete-blocked > td {
|
|
background-color: var(--mud-palette-error-lighten, #ffebee) !important;
|
|
}
|
|
</style>
|
|
|
|
<MudButton OnClick="DownloadCsvAsync" Disabled="@(!LdapUsers.Any())">Export CSV</MudButton>
|
|
<MudButton OnClick="RefreshMoodleUsers">Refresh Moodle users</MudButton>
|
|
<MudButton OnClick="RefreshLdapUsers">Refresh LDAP users</MudButton>
|
|
<MudButton Color="Color.Error" OnClick="DeleteSelectedMoodleUsers"
|
|
Disabled="@(!SelectedUsers.Any() || AdminState.Value.MoodleUsersDeletionInProgress)">
|
|
Delete selected Moodle users
|
|
</MudButton>
|
|
|
|
<MudText Typo="Typo.h1">User Comparison</MudText>
|
|
|
|
@if (!AdminState.Value.LdapUsersLoaded || !AdminState.Value.MoodleUsersLoaded)
|
|
{
|
|
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
|
|
Loading @string.Join(" and ", PendingUserImports)...
|
|
</MudAlert>
|
|
}
|
|
else
|
|
{
|
|
@if (AdminState.Value.MoodleUsersLoadFailed)
|
|
{
|
|
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined">
|
|
Moodle users could not be loaded. LDAP users are shown without Moodle matches.
|
|
</MudAlert>
|
|
}
|
|
|
|
@if (AdminState.Value.MoodleUsersDeletionInProgress)
|
|
{
|
|
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
|
|
Deleting Moodle users: @AdminState.Value.MoodleUsersDeletedCount of @AdminState.Value.MoodleUsersDeletionTotalCount deleted...
|
|
</MudAlert>
|
|
}
|
|
|
|
@if (!CombinedUsers.Any())
|
|
{
|
|
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
|
|
No users found.
|
|
</MudAlert>
|
|
}
|
|
else
|
|
{
|
|
<MudTable @ref="UserTable" T="CombinedUser" Items="@CombinedUsers" Hover="true" Dense="true" RowsPerPage="25"
|
|
MultiSelection="true" @bind-SelectedItems="SelectedUsers" RowClassFunc="GetRowClass" CustomHeader="true">
|
|
<HeaderContent>
|
|
<MudTHeadRow IgnoreCheckbox="true">
|
|
<MudTh>
|
|
<MudCheckBox T="bool?" Value="CurrentPageSelectionState" ValueChanged="SetCurrentPageSelection"
|
|
aria-label="Select visible users" />
|
|
</MudTh>
|
|
<MudTh><MudTableSortLabel SortBy="SortByUsername">Username</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortBy="SortByClassroom">Classroom</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortBy="SortBySurname">Surname</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortBy="SortByGivenName">Given name</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortBy="SortByRole">Role</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortBy="SortByLdapExistence">Exists in LDAP</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortBy="SortByMoodleExistence">Exists in Moodle</MudTableSortLabel></MudTh>
|
|
</MudTHeadRow>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Username">@context.Username</MudTd>
|
|
<MudTd DataLabel="Classroom">@context.Classroom</MudTd>
|
|
<MudTd DataLabel="Surname">@context.Surname</MudTd>
|
|
<MudTd DataLabel="Given name">@context.GivenName</MudTd>
|
|
<MudTd DataLabel="Role">@context.Role</MudTd>
|
|
<MudTd DataLabel="Exists in LDAP">@(context.ExistsInLdap ? "Yes" : "No")</MudTd>
|
|
<MudTd DataLabel="Exists in Moodle">@(context.ExistsInMoodle ? "Yes" : "No")</MudTd>
|
|
</RowTemplate>
|
|
<PagerContent>
|
|
<MudTablePager />
|
|
</PagerContent>
|
|
</MudTable>
|
|
}
|
|
}
|
|
|
|
@code {
|
|
private static readonly Func<CombinedUser, object> SortByUsername = user => user.Username;
|
|
private static readonly Func<CombinedUser, object> SortByClassroom = user => user.Classroom ?? string.Empty;
|
|
private static readonly Func<CombinedUser, object> SortBySurname = user => user.Surname ?? string.Empty;
|
|
private static readonly Func<CombinedUser, object> SortByGivenName = user => user.GivenName ?? string.Empty;
|
|
private static readonly Func<CombinedUser, object> SortByRole = user => user.Role?.ToString() ?? string.Empty;
|
|
private static readonly Func<CombinedUser, object> SortByLdapExistence = user => user.ExistsInLdap;
|
|
private static readonly Func<CombinedUser, object> SortByMoodleExistence = user => user.ExistsInMoodle;
|
|
|
|
private MudTable<CombinedUser>? UserTable { get; set; }
|
|
private HashSet<CombinedUser> SelectedUsers { get; set; } = [];
|
|
private HashSet<CombinedUser> InvalidDeleteSelection { get; } = [];
|
|
private IEnumerable<Wollmilchsau.Models.LdapUser> LdapUsers => AdminState.Value.LdapUsers.Values;
|
|
|
|
private IEnumerable<CombinedUser> CurrentPageUsers
|
|
{
|
|
get
|
|
{
|
|
if (UserTable is null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return UserTable.FilteredItems
|
|
.Skip(UserTable.CurrentPage * UserTable.RowsPerPage)
|
|
.Take(UserTable.RowsPerPage);
|
|
}
|
|
}
|
|
|
|
private bool? CurrentPageSelectionState
|
|
{
|
|
get
|
|
{
|
|
var users = CurrentPageUsers.ToArray();
|
|
if (users.Length == 0 || !users.Any(SelectedUsers.Contains))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return users.All(SelectedUsers.Contains) ? true : null;
|
|
}
|
|
}
|
|
|
|
private IEnumerable<string> PendingUserImports
|
|
{
|
|
get
|
|
{
|
|
if (!AdminState.Value.LdapUsersLoaded)
|
|
{
|
|
yield return "LDAP";
|
|
}
|
|
|
|
if (!AdminState.Value.MoodleUsersLoaded)
|
|
{
|
|
yield return "Moodle";
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<CombinedUser> CombinedUsers
|
|
{
|
|
get
|
|
{
|
|
var ldapUsers = AdminState.Value.LdapUsers;
|
|
var moodleUsers = AdminState.Value.MoodleUsers.Values.ToArray();
|
|
var moodleUsersByUsername = moodleUsers
|
|
.Where(moodleUser => !string.IsNullOrWhiteSpace(moodleUser.Username))
|
|
.GroupBy(moodleUser => moodleUser.Username!, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
|
|
|
|
return ldapUsers.Values
|
|
.Select(ldapUser => CreateCombinedUser(
|
|
ldapUser,
|
|
moodleUsersByUsername.GetValueOrDefault(ldapUser.Username)))
|
|
.Concat(moodleUsers
|
|
.Where(moodleUser =>
|
|
string.IsNullOrWhiteSpace(moodleUser.Username) ||
|
|
!ldapUsers.ContainsKey(moodleUser.Username))
|
|
.Select(moodleUser => CreateCombinedUser(null, moodleUser)))
|
|
.OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
base.OnInitialized();
|
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.GetAllMoodleUsers());
|
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.GetAllLdapUsers());
|
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.GetAllClassrooms());
|
|
}
|
|
|
|
private Task DownloadCsvAsync()
|
|
{
|
|
var csv = string.Join("\r\n", LdapUsers
|
|
.Where(user =>
|
|
user.Role == Wollmilchsau.Models.LdapUserRole.Student &&
|
|
!string.IsNullOrWhiteSpace(user.Classroom))
|
|
.Select(user => string.Join(';',
|
|
user.GivenName ?? string.Empty,
|
|
user.Surname ?? string.Empty,
|
|
user.Classroom,
|
|
user.Username)));
|
|
|
|
return JsRuntime.InvokeVoidAsync("downloadTextFile", "ldap-users.csv", csv).AsTask();
|
|
}
|
|
|
|
private void RefreshMoodleUsers()
|
|
{
|
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.RefreshMoodleUsers());
|
|
}
|
|
|
|
private void RefreshLdapUsers()
|
|
{
|
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.RefreshLdapUsers());
|
|
}
|
|
|
|
private void SetCurrentPageSelection(bool? selected)
|
|
{
|
|
var users = CurrentPageUsers.ToArray();
|
|
if (selected == true)
|
|
{
|
|
SelectedUsers.UnionWith(users);
|
|
}
|
|
else
|
|
{
|
|
SelectedUsers.ExceptWith(users);
|
|
}
|
|
}
|
|
|
|
private void DeleteSelectedMoodleUsers()
|
|
{
|
|
InvalidDeleteSelection.Clear();
|
|
|
|
var protectedUsers = SelectedUsers
|
|
.Where(user => user.ExistsInLdap || ContainsAdminTerm(user) || IsManualMoodleUser(user))
|
|
.ToArray();
|
|
if (protectedUsers.Length > 0)
|
|
{
|
|
InvalidDeleteSelection.UnionWith(protectedUsers);
|
|
Snackbar.Add("No users were deleted because the selection contains LDAP users, manual Moodle users, or names containing 'admin'.", Severity.Error);
|
|
return;
|
|
}
|
|
|
|
var moodleUserIds = SelectedUsers
|
|
.Where(user => user.ExistsInMoodle && user.MoodleUserId is not null)
|
|
.Select(user => user.MoodleUserId!.Value)
|
|
.Distinct()
|
|
.ToArray();
|
|
|
|
if (moodleUserIds.Length == 0)
|
|
{
|
|
Snackbar.Add("No Moodle users were selected for deletion.", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.DeleteMoodleUsers(moodleUserIds));
|
|
SelectedUsers = [];
|
|
}
|
|
|
|
private string GetRowClass(CombinedUser user, int rowIndex)
|
|
{
|
|
return InvalidDeleteSelection.Contains(user) ? "moodle-delete-blocked" : string.Empty;
|
|
}
|
|
|
|
private static bool ContainsAdminTerm(CombinedUser user)
|
|
{
|
|
return user.Username.Contains("admin", StringComparison.OrdinalIgnoreCase) ||
|
|
user.Surname?.Contains("admin", StringComparison.OrdinalIgnoreCase) == true ||
|
|
user.GivenName?.Contains("admin", StringComparison.OrdinalIgnoreCase) == true ||
|
|
user.MoodleFullname?.Contains("admin", StringComparison.OrdinalIgnoreCase) == true;
|
|
}
|
|
|
|
private static bool IsManualMoodleUser(CombinedUser user)
|
|
{
|
|
return string.Equals(user.MoodleAuth, "manual", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static CombinedUser CreateCombinedUser(
|
|
Wollmilchsau.Models.LdapUser? ldapUser,
|
|
Wollmilchsau.Models.MoodleUser? moodleUser)
|
|
{
|
|
return new CombinedUser(
|
|
ldapUser?.Username ?? moodleUser?.Username ?? string.Empty,
|
|
moodleUser?.Id,
|
|
ldapUser?.Classroom,
|
|
ldapUser?.Surname ?? moodleUser?.Lastname,
|
|
ldapUser?.GivenName ?? moodleUser?.Firstname,
|
|
moodleUser?.Fullname,
|
|
moodleUser?.Auth,
|
|
ldapUser?.Role,
|
|
ldapUser is not null,
|
|
moodleUser is not null);
|
|
}
|
|
|
|
private sealed record CombinedUser(
|
|
string Username,
|
|
int? MoodleUserId,
|
|
string? Classroom,
|
|
string? Surname,
|
|
string? GivenName,
|
|
string? MoodleFullname,
|
|
string? MoodleAuth,
|
|
Wollmilchsau.Models.LdapUserRole? Role,
|
|
bool ExistsInLdap,
|
|
bool ExistsInMoodle);
|
|
}
|