Bugfixes and changes to the Admin Page

This commit is contained in:
Rhywden
2026-08-24 15:23:53 +02:00
parent c071489eec
commit cf145c299c
9 changed files with 396 additions and 16 deletions
@@ -5,17 +5,30 @@
@inject Fluxor.IDispatcher Dispatcher @inject Fluxor.IDispatcher Dispatcher
@inject Fluxor.IState<Wollmilchsau.Store.AdminStore.AdminState> AdminState @inject Fluxor.IState<Wollmilchsau.Store.AdminStore.AdminState> AdminState
@inject IJSRuntime JsRuntime @inject IJSRuntime JsRuntime
@inject ISnackbar Snackbar
<PageTitle>User Comparison</PageTitle> <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="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> <MudText Typo="Typo.h1">User Comparison</MudText>
@if (!AdminState.Value.LdapUsersLoaded || !AdminState.Value.MoodleUsersLoaded) @if (!AdminState.Value.LdapUsersLoaded || !AdminState.Value.MoodleUsersLoaded)
{ {
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined"> <MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
Loading @string.Join(" and ", PendingUserImports, " users")... Loading @string.Join(" and ", PendingUserImports)...
</MudAlert> </MudAlert>
} }
else else
@@ -27,6 +40,13 @@ else
</MudAlert> </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()) @if (!CombinedUsers.Any())
{ {
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined"> <MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
@@ -35,8 +55,14 @@ else
} }
else else
{ {
<MudTable T="CombinedUser" Items="@CombinedUsers" Hover="true" Dense="true" RowsPerPage="25"> <MudTable @ref="UserTable" T="CombinedUser" Items="@CombinedUsers" Hover="true" Dense="true" RowsPerPage="25"
MultiSelection="true" @bind-SelectedItems="SelectedUsers" RowClassFunc="GetRowClass" CustomHeader="true">
<HeaderContent> <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="SortByUsername">Username</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="SortByClassroom">Classroom</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortBy="SortByClassroom">Classroom</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="SortBySurname">Surname</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortBy="SortBySurname">Surname</MudTableSortLabel></MudTh>
@@ -44,6 +70,7 @@ else
<MudTh><MudTableSortLabel SortBy="SortByRole">Role</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortBy="SortByRole">Role</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="SortByLdapExistence">Exists in LDAP</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortBy="SortByLdapExistence">Exists in LDAP</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="SortByMoodleExistence">Exists in Moodle</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortBy="SortByMoodleExistence">Exists in Moodle</MudTableSortLabel></MudTh>
</MudTHeadRow>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd DataLabel="Username">@context.Username</MudTd> <MudTd DataLabel="Username">@context.Username</MudTd>
@@ -70,8 +97,40 @@ else
private static readonly Func<CombinedUser, object> SortByLdapExistence = user => user.ExistsInLdap; private static readonly Func<CombinedUser, object> SortByLdapExistence = user => user.ExistsInLdap;
private static readonly Func<CombinedUser, object> SortByMoodleExistence = user => user.ExistsInMoodle; 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<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 private IEnumerable<string> PendingUserImports
{ {
get get
@@ -123,25 +182,101 @@ else
private Task DownloadCsvAsync() private Task DownloadCsvAsync()
{ {
var csv = string.Join("\r\n", LdapUsers var csv = string.Join("\r\n", LdapUsers
.Where(user => user.Role == Wollmilchsau.Models.LdapUserRole.Student) .Where(user =>
user.Role == Wollmilchsau.Models.LdapUserRole.Student &&
!string.IsNullOrWhiteSpace(user.Classroom))
.Select(user => string.Join(';', .Select(user => string.Join(';',
user.GivenName ?? string.Empty, user.GivenName ?? string.Empty,
user.Surname ?? string.Empty, user.Surname ?? string.Empty,
user.Classroom ?? string.Empty, user.Classroom,
user.Username))); user.Username)));
return JsRuntime.InvokeVoidAsync("downloadTextFile", "ldap-users.csv", csv).AsTask(); 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( private static CombinedUser CreateCombinedUser(
Wollmilchsau.Models.LdapUser? ldapUser, Wollmilchsau.Models.LdapUser? ldapUser,
Wollmilchsau.Models.MoodleUser? moodleUser) Wollmilchsau.Models.MoodleUser? moodleUser)
{ {
return new CombinedUser( return new CombinedUser(
ldapUser?.Username ?? moodleUser?.Username ?? string.Empty, ldapUser?.Username ?? moodleUser?.Username ?? string.Empty,
moodleUser?.Id,
ldapUser?.Classroom, ldapUser?.Classroom,
ldapUser?.Surname ?? moodleUser?.Lastname, ldapUser?.Surname ?? moodleUser?.Lastname,
ldapUser?.GivenName ?? moodleUser?.Firstname, ldapUser?.GivenName ?? moodleUser?.Firstname,
moodleUser?.Fullname,
moodleUser?.Auth,
ldapUser?.Role, ldapUser?.Role,
ldapUser is not null, ldapUser is not null,
moodleUser is not null); moodleUser is not null);
@@ -149,9 +284,12 @@ else
private sealed record CombinedUser( private sealed record CombinedUser(
string Username, string Username,
int? MoodleUserId,
string? Classroom, string? Classroom,
string? Surname, string? Surname,
string? GivenName, string? GivenName,
string? MoodleFullname,
string? MoodleAuth,
Wollmilchsau.Models.LdapUserRole? Role, Wollmilchsau.Models.LdapUserRole? Role,
bool ExistsInLdap, bool ExistsInLdap,
bool ExistsInMoodle); bool ExistsInMoodle);
+1 -1
View File
@@ -99,7 +99,7 @@ builder.Services.AddHttpClient<IMoodleService, MoodleService>((serviceProvider,
{ {
var options = serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<MoodleOptions>>().Value; var options = serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<MoodleOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl, UriKind.Absolute); client.BaseAddress = new Uri(options.BaseUrl, UriKind.Absolute);
client.Timeout = TimeSpan.FromSeconds(30); client.Timeout = TimeSpan.FromMinutes(2);
}); });
// Add services to the container. // Add services to the container.
+12 -2
View File
@@ -115,7 +115,6 @@ public sealed class LdapService(IOptions<LdapOptions> options) : ILdapService
SearchScope.Subtree, SearchScope.Subtree,
"uid", "uid",
"sn", "sn",
"departmentNumber",
"givenName", "givenName",
"memberOf"); "memberOf");
var response = (SearchResponse)connection.SendRequest(request); var response = (SearchResponse)connection.SendRequest(request);
@@ -129,7 +128,7 @@ public sealed class LdapService(IOptions<LdapOptions> options) : ILdapService
{ {
var username = GetFirstAttributeValue(entry, "uid") ?? throw new InvalidOperationException($"LDAP user entry '{entry.DistinguishedName}' is missing uid."); var username = GetFirstAttributeValue(entry, "uid") ?? throw new InvalidOperationException($"LDAP user entry '{entry.DistinguishedName}' is missing uid.");
var surname = CapitalizeFirstLetter(GetFirstAttributeValue(entry, "sn")); var surname = CapitalizeFirstLetter(GetFirstAttributeValue(entry, "sn"));
var classroom = GetFirstAttributeValue(entry, "departmentNumber"); var classroom = GetClassroomFromMembership(entry);
var givenName = GetFirstAttributeValue(entry, "givenName"); var givenName = GetFirstAttributeValue(entry, "givenName");
var role = DetermineUserRole(entry, studentRoleDn, teacherRoleDn); var role = DetermineUserRole(entry, studentRoleDn, teacherRoleDn);
@@ -144,6 +143,17 @@ public sealed class LdapService(IOptions<LdapOptions> options) : ILdapService
role); role);
} }
private static string? GetClassroomFromMembership(SearchResultEntry entry)
{
const string classroomGroupPrefix = "CN=klasse.";
return GetAttributeValues(entry, "memberOf")
.Select(groupDn => groupDn.Split(',', 2)[0])
.Where(commonName => commonName.StartsWith(classroomGroupPrefix, StringComparison.OrdinalIgnoreCase))
.Select(commonName => commonName[classroomGroupPrefix.Length..].ToUpperInvariant())
.FirstOrDefault();
}
private static LdapUserRole DetermineUserRole(SearchResultEntry entry, string studentRoleDn, string teacherRoleDn) private static LdapUserRole DetermineUserRole(SearchResultEntry entry, string studentRoleDn, string teacherRoleDn)
{ {
var roles = GetAttributeValues(entry, "memberOf"); var roles = GetAttributeValues(entry, "memberOf");
@@ -5,4 +5,5 @@ namespace Wollmilchsau.Services.Moodle;
public interface IMoodleService public interface IMoodleService
{ {
Task<IReadOnlyCollection<MoodleUser>> GetAllUsers(CancellationToken cancellationToken = default); Task<IReadOnlyCollection<MoodleUser>> GetAllUsers(CancellationToken cancellationToken = default);
Task DeleteUsers(IReadOnlyCollection<int> userIds, CancellationToken cancellationToken = default);
} }
@@ -1,4 +1,5 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Wollmilchsau.Models; using Wollmilchsau.Models;
@@ -39,9 +40,43 @@ public sealed class MoodleService(HttpClient httpClient, IOptions<MoodleOptions>
return result.Users ?? throw new InvalidOperationException("Moodle user search response did not contain users."); return result.Users ?? throw new InvalidOperationException("Moodle user search response did not contain users.");
} }
public async Task DeleteUsers(IReadOnlyCollection<int> userIds, CancellationToken cancellationToken = default)
{
if (userIds.Count == 0)
{
throw new ArgumentException("At least one Moodle user ID is required.", nameof(userIds));
}
var query = new QueryBuilder
{
{ "wstoken", _options.WsToken },
{ "wsfunction", "core_user_delete_users" },
{ "moodlewsrestformat", "json" }
};
foreach (var (userId, index) in userIds.Distinct().Select((userId, index) => (userId, index)))
{
query.Add($"userids[{index}]", userId.ToString());
}
using var response = await httpClient.GetAsync(query.ToQueryString().Value, cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<MoodleDeletionResponse>(cancellationToken);
if (!string.IsNullOrWhiteSpace(result?.Exception))
{
throw new InvalidOperationException($"Moodle user deletion failed with {result.ErrorCode ?? "an unspecified error"}: {result.Message ?? result.Exception}.");
}
}
private sealed record MoodleUserSearchResponse( private sealed record MoodleUserSearchResponse(
IReadOnlyCollection<MoodleUser>? Users, IReadOnlyCollection<MoodleUser>? Users,
string? Exception, string? Exception,
string? ErrorCode, string? ErrorCode,
string? Message); string? Message);
private sealed record MoodleDeletionResponse(
string? Exception,
string? ErrorCode,
string? Message);
} }
@@ -3,10 +3,17 @@
namespace Wollmilchsau.Store.AdminStore; namespace Wollmilchsau.Store.AdminStore;
public sealed record GetAllMoodleUsers(); public sealed record GetAllMoodleUsers();
public sealed record RefreshMoodleUsers();
public sealed record SetMoodleUsers(IReadOnlyCollection<MoodleUser> Users); public sealed record SetMoodleUsers(IReadOnlyCollection<MoodleUser> Users);
public sealed record SetMoodleUsersLoadFailed(); public sealed record SetMoodleUsersLoadFailed();
public sealed record DeleteMoodleUsers(IReadOnlyCollection<int> UserIds);
public sealed record MoodleUsersDeletionStarted(int TotalUsers);
public sealed record MoodleUsersDeleted(IReadOnlyCollection<int> UserIds);
public sealed record MoodleUsersDeletionFailed();
public sealed record MoodleUsersDeletionCompleted();
public sealed record GetAllElementUsers(); public sealed record GetAllElementUsers();
public sealed record GetAllLdapUsers(); public sealed record GetAllLdapUsers();
public sealed record RefreshLdapUsers();
public sealed record SetLdapUsers(IReadOnlyCollection<LdapUser> Users); public sealed record SetLdapUsers(IReadOnlyCollection<LdapUser> Users);
public sealed record GetAllClassrooms(); public sealed record GetAllClassrooms();
public sealed record SetClassrooms(IReadOnlyCollection<Classroom> Classrooms); public sealed record SetClassrooms(IReadOnlyCollection<Classroom> Classrooms);
+146 -1
View File
@@ -1,14 +1,17 @@
using System.Text.Json; using System.Text.Json;
using Fluxor; using Fluxor;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using MudBlazor;
using StackExchange.Redis;
using Wollmilchsau.Models; using Wollmilchsau.Models;
using Wollmilchsau.Services.Ldap; using Wollmilchsau.Services.Ldap;
using Wollmilchsau.Services.Moodle; using Wollmilchsau.Services.Moodle;
namespace Wollmilchsau.Store.AdminStore; namespace Wollmilchsau.Store.AdminStore;
public sealed class AdminEffects(IMoodleService moodleService, ILdapService ldapService, IDistributedCache cache) public sealed class AdminEffects(IMoodleService moodleService, ILdapService ldapService, IDistributedCache cache, ISnackbar snackbar)
{ {
private const int MoodleDeleteBatchSize = 5;
private const string MoodleUsersCacheKey = "admin:moodle-users"; private const string MoodleUsersCacheKey = "admin:moodle-users";
private const string LdapUsersCacheKey = "admin:ldap-users"; private const string LdapUsersCacheKey = "admin:ldap-users";
private const string ClassroomsCacheKey = "admin:classrooms"; private const string ClassroomsCacheKey = "admin:classrooms";
@@ -74,6 +77,124 @@ public sealed class AdminEffects(IMoodleService moodleService, ILdapService ldap
return users; return users;
} }
[EffectMethod]
public async Task HandleRefreshMoodleUsers(RefreshMoodleUsers action, IDispatcher dispatcher)
{
try
{
await cache.RemoveAsync(MoodleUsersCacheKey);
dispatcher.Dispatch(new SetMoodleUsers(await LoadAndCacheMoodleUsers()));
snackbar.Add("Moodle users refreshed.", Severity.Success);
}
catch (RedisException)
{
snackbar.Add("Moodle users could not be refreshed because the cache could not be cleared.", Severity.Error);
}
catch (HttpRequestException)
{
snackbar.Add("Moodle users could not be refreshed.", Severity.Error);
}
catch (JsonException)
{
snackbar.Add("Moodle users could not be refreshed.", Severity.Error);
}
catch (InvalidOperationException)
{
snackbar.Add("Moodle users could not be refreshed.", Severity.Error);
}
catch (TaskCanceledException)
{
snackbar.Add("Moodle user refresh timed out.", Severity.Error);
}
}
[EffectMethod]
public async Task HandleDeleteMoodleUsers(DeleteMoodleUsers action, IDispatcher dispatcher)
{
var batches = action.UserIds
.Distinct()
.Chunk(MoodleDeleteBatchSize)
.ToArray();
if (batches.Length == 0)
{
snackbar.Add("No Moodle users were selected for deletion.", Severity.Warning);
return;
}
var totalUserCount = batches.Sum(batch => batch.Length);
dispatcher.Dispatch(new MoodleUsersDeletionStarted(totalUserCount));
try
{
await cache.RemoveAsync(MoodleUsersCacheKey);
}
catch (RedisException)
{
ReportMoodleDeletionFailure(dispatcher, "Moodle deletion did not start because the cache could not be invalidated.");
return;
}
var deletedUserCount = 0;
foreach (var batch in batches)
{
if (!await TryDeleteMoodleUserBatch(batch, deletedUserCount, totalUserCount, dispatcher))
{
return;
}
dispatcher.Dispatch(new MoodleUsersDeleted(batch));
deletedUserCount += batch.Length;
}
dispatcher.Dispatch(new MoodleUsersDeletionCompleted());
snackbar.Add($"Deleted {action.UserIds.Distinct().Count()} Moodle user(s).", Severity.Success);
}
private async Task<bool> TryDeleteMoodleUserBatch(
IReadOnlyCollection<int> userIds,
int deletedUserCount,
int totalUserCount,
IDispatcher dispatcher)
{
try
{
await moodleService.DeleteUsers(userIds);
return true;
}
catch (HttpRequestException)
{
ReportMoodleDeletionFailure(dispatcher, deletedUserCount, totalUserCount);
}
catch (InvalidOperationException)
{
ReportMoodleDeletionFailure(dispatcher, deletedUserCount, totalUserCount);
}
catch (TaskCanceledException)
{
ReportMoodleDeletionFailure(dispatcher, deletedUserCount, totalUserCount);
}
catch (JsonException)
{
ReportMoodleDeletionFailure(dispatcher, deletedUserCount, totalUserCount);
}
return false;
}
private void ReportMoodleDeletionFailure(IDispatcher dispatcher, int deletedUserCount, int totalUserCount)
{
ReportMoodleDeletionFailure(
dispatcher,
$"Moodle deletion stopped after deleting {deletedUserCount} of {totalUserCount} user(s). Later users were not processed.");
}
private void ReportMoodleDeletionFailure(IDispatcher dispatcher, string message)
{
dispatcher.Dispatch(new MoodleUsersDeletionFailed());
snackbar.Add(message, Severity.Error);
}
[EffectMethod] [EffectMethod]
public async Task HandleGetAllLdapUsers(GetAllLdapUsers action, IDispatcher dispatcher) public async Task HandleGetAllLdapUsers(GetAllLdapUsers action, IDispatcher dispatcher)
{ {
@@ -121,6 +242,30 @@ public sealed class AdminEffects(IMoodleService moodleService, ILdapService ldap
return loadedUsers; return loadedUsers;
} }
[EffectMethod]
public async Task HandleRefreshLdapUsers(RefreshLdapUsers action, IDispatcher dispatcher)
{
try
{
await cache.RemoveAsync(LdapUsersCacheKey);
var users = await LoadAndCacheLdapUsers(await ldapService.GetUserCount());
dispatcher.Dispatch(new SetLdapUsers(users));
snackbar.Add("LDAP users refreshed.", Severity.Success);
}
catch (RedisException)
{
snackbar.Add("LDAP users could not be refreshed because the cache could not be cleared.", Severity.Error);
}
catch (System.DirectoryServices.Protocols.LdapException)
{
snackbar.Add("LDAP users could not be refreshed.", Severity.Error);
}
catch (InvalidOperationException)
{
snackbar.Add("LDAP users could not be refreshed.", Severity.Error);
}
}
[EffectMethod] [EffectMethod]
public async Task HandleGetAllClassrooms(GetAllClassrooms action, IDispatcher dispatcher) public async Task HandleGetAllClassrooms(GetAllClassrooms action, IDispatcher dispatcher)
{ {
+42 -1
View File
@@ -12,7 +12,10 @@ public static class AdminReducers
{ {
MoodleUsers = action.Users.ToImmutableDictionary(user => user.Id), MoodleUsers = action.Users.ToImmutableDictionary(user => user.Id),
MoodleUsersLoaded = true, MoodleUsersLoaded = true,
MoodleUsersLoadFailed = false MoodleUsersLoadFailed = false,
MoodleUsersDeletionInProgress = false,
MoodleUsersDeletedCount = 0,
MoodleUsersDeletionTotalCount = 0
}; };
} }
@@ -27,6 +30,44 @@ public static class AdminReducers
}; };
} }
[ReducerMethod]
public static AdminState ReduceMoodleUsersDeletionStarted(AdminState state, MoodleUsersDeletionStarted action)
{
return state with
{
MoodleUsersDeletionInProgress = true,
MoodleUsersDeletedCount = 0,
MoodleUsersDeletionTotalCount = action.TotalUsers
};
}
[ReducerMethod]
public static AdminState ReduceMoodleUsersDeleted(AdminState state, MoodleUsersDeleted action)
{
return state with
{
MoodleUsers = state.MoodleUsers.RemoveRange(action.UserIds),
MoodleUsersDeletedCount = state.MoodleUsersDeletedCount + action.UserIds.Count
};
}
[ReducerMethod]
public static AdminState ReduceMoodleUsersDeletionFailed(AdminState state, MoodleUsersDeletionFailed action)
{
return state with { MoodleUsersDeletionInProgress = false };
}
[ReducerMethod]
public static AdminState ReduceMoodleUsersDeletionCompleted(AdminState state, MoodleUsersDeletionCompleted action)
{
return state with
{
MoodleUsersDeletionInProgress = false,
MoodleUsersDeletedCount = 0,
MoodleUsersDeletionTotalCount = 0
};
}
[ReducerMethod] [ReducerMethod]
public static AdminState ReduceSetLdapUsers(AdminState state, SetLdapUsers action) public static AdminState ReduceSetLdapUsers(AdminState state, SetLdapUsers action)
{ {
@@ -12,6 +12,9 @@ public sealed record AdminState
public ImmutableDictionary<int, MoodleUser> MoodleUsers { get; init; } = []; public ImmutableDictionary<int, MoodleUser> MoodleUsers { get; init; } = [];
public bool MoodleUsersLoaded { get; init; } public bool MoodleUsersLoaded { get; init; }
public bool MoodleUsersLoadFailed { get; init; } public bool MoodleUsersLoadFailed { get; init; }
public bool MoodleUsersDeletionInProgress { get; init; }
public int MoodleUsersDeletedCount { get; init; }
public int MoodleUsersDeletionTotalCount { get; init; }
public ImmutableDictionary<string, LdapUser> LdapUsers { get; init; } = []; public ImmutableDictionary<string, LdapUser> LdapUsers { get; init; } = [];
public ImmutableList<Classroom> Classrooms { get; init; } = []; public ImmutableList<Classroom> Classrooms { get; init; } = [];
public bool LdapUsersLoaded { get; init; } public bool LdapUsersLoaded { get; init; }