Added loading of Moodle Users and displaying them
This commit is contained in:
@@ -6,62 +6,95 @@
|
||||
@inject Fluxor.IState<Wollmilchsau.Store.AdminStore.AdminState> AdminState
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<PageTitle>LDAP Users</PageTitle>
|
||||
<PageTitle>User Comparison</PageTitle>
|
||||
|
||||
<button class="btn btn-primary" @onclick="DownloadCsvAsync" disabled="@(!Users.Any())">Export CSV</button>
|
||||
<button class="btn btn-primary" @onclick="DownloadCsvAsync" disabled="@(!LdapUsers.Any())">Export CSV</button>
|
||||
|
||||
<h1>LDAP Users</h1>
|
||||
<h1>User Comparison</h1>
|
||||
|
||||
@if (!AdminState.Value.LdapUsersLoaded)
|
||||
@if (!AdminState.Value.LdapUsersLoaded || !AdminState.Value.MoodleUsersLoaded)
|
||||
{
|
||||
<p><em>Loading users...</em></p>
|
||||
}
|
||||
else if (!Users.Any())
|
||||
else
|
||||
{
|
||||
<p>No LDAP users found.</p>
|
||||
@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 (!CombinedUsers.Any())
|
||||
{
|
||||
<p>No users found.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Surname</th>
|
||||
<th>Classroom</th>
|
||||
<th>Given name</th>
|
||||
<th>Incomplete</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in Users)
|
||||
{
|
||||
<tr>
|
||||
<td>@user.Username</td>
|
||||
<td>@user.Surname</td>
|
||||
<td>@user.Classroom</td>
|
||||
<td>@user.GivenName</td>
|
||||
<td>@user.Incomplete</td>
|
||||
<td>@user.Role</td>
|
||||
</tr>
|
||||
<MudTable T="CombinedUser" Items="@CombinedUsers" Hover="true" Dense="true" RowsPerPage="25">
|
||||
<HeaderContent>
|
||||
<MudTh>Username</MudTh>
|
||||
<MudTh>Classroom</MudTh>
|
||||
<MudTh>Surname</MudTh>
|
||||
<MudTh>Given name</MudTh>
|
||||
<MudTh>Role</MudTh>
|
||||
<MudTh>Exists in LDAP</MudTh>
|
||||
<MudTh>Exists in Moodle</MudTh>
|
||||
</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>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private IEnumerable<Wollmilchsau.Models.LdapUser> Users => AdminState.Value.LdapUsers.Values;
|
||||
private IEnumerable<Wollmilchsau.Models.LdapUser> LdapUsers => AdminState.Value.LdapUsers.Values;
|
||||
|
||||
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", Users
|
||||
var csv = string.Join("\r\n", LdapUsers
|
||||
.Where(user => user.Role == Wollmilchsau.Models.LdapUserRole.Student)
|
||||
.Select(user => string.Join(';',
|
||||
user.GivenName ?? string.Empty,
|
||||
@@ -71,4 +104,27 @@ else
|
||||
|
||||
return JsRuntime.InvokeVoidAsync("downloadTextFile", "ldap-users.csv", csv).AsTask();
|
||||
}
|
||||
|
||||
private static CombinedUser CreateCombinedUser(
|
||||
Wollmilchsau.Models.LdapUser? ldapUser,
|
||||
Wollmilchsau.Models.MoodleUser? moodleUser)
|
||||
{
|
||||
return new CombinedUser(
|
||||
ldapUser?.Username ?? moodleUser?.Username ?? string.Empty,
|
||||
ldapUser?.Classroom,
|
||||
ldapUser?.Surname ?? moodleUser?.Lastname,
|
||||
ldapUser?.GivenName ?? moodleUser?.Firstname,
|
||||
ldapUser?.Role,
|
||||
ldapUser is not null,
|
||||
moodleUser is not null);
|
||||
}
|
||||
|
||||
private sealed record CombinedUser(
|
||||
string Username,
|
||||
string? Classroom,
|
||||
string? Surname,
|
||||
string? GivenName,
|
||||
Wollmilchsau.Models.LdapUserRole? Role,
|
||||
bool ExistsInLdap,
|
||||
bool ExistsInMoodle);
|
||||
}
|
||||
@@ -2,4 +2,25 @@
|
||||
|
||||
public sealed record MoodleUser
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string? Username { get; init; }
|
||||
public string? Firstname { get; init; }
|
||||
public string? Lastname { get; init; }
|
||||
public string? Fullname { get; init; }
|
||||
public string? Initials { get; init; }
|
||||
public string? Department { get; init; }
|
||||
public long Firstaccess { get; init; }
|
||||
public long Lastaccess { get; init; }
|
||||
public string? Auth { get; init; }
|
||||
public bool Suspended { get; init; }
|
||||
public bool Confirmed { get; init; }
|
||||
public string? Lang { get; init; }
|
||||
public string? Theme { get; init; }
|
||||
public string? Timezone { get; init; }
|
||||
public int Mailformat { get; init; }
|
||||
public int Trackforums { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public int Descriptionformat { get; init; }
|
||||
public string? Profileimageurlsmall { get; init; }
|
||||
public string? Profileimageurl { get; init; }
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Text.Json;
|
||||
using Wollmilchsau.Components;
|
||||
using Wollmilchsau.Exceptions;
|
||||
using Wollmilchsau.Services.Ldap;
|
||||
using Wollmilchsau.Services.Moodle;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -90,6 +91,17 @@ builder.Services.AddOptions<LdapOptions>()
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddScoped<ILdapService, LdapService>();
|
||||
|
||||
builder.Services.AddOptions<MoodleOptions>()
|
||||
.BindConfiguration("Moodle")
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddHttpClient<IMoodleService, MoodleService>((serviceProvider, client) =>
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<MoodleOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.BaseUrl, UriKind.Absolute);
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Wollmilchsau.Models;
|
||||
|
||||
namespace Wollmilchsau.Services.Moodle;
|
||||
|
||||
public interface IMoodleService
|
||||
{
|
||||
Task<IReadOnlyCollection<MoodleUser>> GetAllUsers(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Wollmilchsau.Services.Moodle;
|
||||
|
||||
public sealed class MoodleOptions
|
||||
{
|
||||
[Required]
|
||||
[Url]
|
||||
public string BaseUrl { get; init; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string WsToken { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Wollmilchsau.Models;
|
||||
|
||||
namespace Wollmilchsau.Services.Moodle;
|
||||
|
||||
public sealed class MoodleService(HttpClient httpClient, IOptions<MoodleOptions> options) : IMoodleService
|
||||
{
|
||||
private readonly MoodleOptions _options = options.Value;
|
||||
|
||||
public async Task<IReadOnlyCollection<MoodleUser>> GetAllUsers(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var requestUri = QueryHelpers.AddQueryString(
|
||||
string.Empty,
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
["wstoken"] = _options.WsToken,
|
||||
["wsfunction"] = "core_user_get_users",
|
||||
["moodlewsrestformat"] = "json",
|
||||
["criteria[0][key]"] = "lastname",
|
||||
["criteria[0][value]"] = "%"
|
||||
});
|
||||
|
||||
using var response = await httpClient.GetAsync(requestUri, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MoodleUserSearchResponse>(cancellationToken);
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Moodle returned an empty user search response.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.Exception))
|
||||
{
|
||||
throw new InvalidOperationException($"Moodle user search failed with {result.ErrorCode ?? "an unspecified error"}: {result.Message ?? result.Exception}.");
|
||||
}
|
||||
|
||||
return result.Users ?? throw new InvalidOperationException("Moodle user search response did not contain users.");
|
||||
}
|
||||
|
||||
private sealed record MoodleUserSearchResponse(
|
||||
IReadOnlyCollection<MoodleUser>? Users,
|
||||
string? Exception,
|
||||
string? ErrorCode,
|
||||
string? Message);
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace Wollmilchsau.Store.AdminStore;
|
||||
|
||||
public sealed record GetAllMoodleUsers();
|
||||
public sealed record SetMoodleUsers(IReadOnlyCollection<MoodleUser> Users);
|
||||
public sealed record SetMoodleUsersLoadFailed();
|
||||
public sealed record GetAllElementUsers();
|
||||
public sealed record GetAllLdapUsers();
|
||||
public sealed record SetLdapUsers(IReadOnlyCollection<LdapUser> Users);
|
||||
|
||||
@@ -3,14 +3,77 @@ using Fluxor;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Wollmilchsau.Models;
|
||||
using Wollmilchsau.Services.Ldap;
|
||||
using Wollmilchsau.Services.Moodle;
|
||||
|
||||
namespace Wollmilchsau.Store.AdminStore;
|
||||
|
||||
public sealed class AdminEffects(ILdapService ldapService, IDistributedCache cache)
|
||||
public sealed class AdminEffects(IMoodleService moodleService, ILdapService ldapService, IDistributedCache cache)
|
||||
{
|
||||
private const string MoodleUsersCacheKey = "admin:moodle-users";
|
||||
private const string LdapUsersCacheKey = "admin:ldap-users";
|
||||
private const string ClassroomsCacheKey = "admin:classrooms";
|
||||
|
||||
[EffectMethod]
|
||||
public async Task HandleGetAllMoodleUsers(GetAllMoodleUsers action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cachedUsers = await cache.GetStringAsync(MoodleUsersCacheKey);
|
||||
IReadOnlyCollection<MoodleUser> users;
|
||||
|
||||
if (cachedUsers is not null)
|
||||
{
|
||||
var deserializedUsers = JsonSerializer.Deserialize<MoodleUser[]>(cachedUsers);
|
||||
if (deserializedUsers is not null)
|
||||
{
|
||||
users = deserializedUsers;
|
||||
}
|
||||
else
|
||||
{
|
||||
await cache.RemoveAsync(MoodleUsersCacheKey);
|
||||
users = await LoadAndCacheMoodleUsers();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
users = await LoadAndCacheMoodleUsers();
|
||||
}
|
||||
|
||||
dispatcher.Dispatch(new SetMoodleUsers(users));
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
dispatcher.Dispatch(new SetMoodleUsersLoadFailed());
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
dispatcher.Dispatch(new SetMoodleUsersLoadFailed());
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
dispatcher.Dispatch(new SetMoodleUsersLoadFailed());
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
dispatcher.Dispatch(new SetMoodleUsersLoadFailed());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<MoodleUser>> LoadAndCacheMoodleUsers()
|
||||
{
|
||||
var users = await moodleService.GetAllUsers();
|
||||
|
||||
await cache.SetStringAsync(
|
||||
MoodleUsersCacheKey,
|
||||
JsonSerializer.Serialize(users),
|
||||
new DistributedCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = TimeSpan.FromMinutes(10)
|
||||
});
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
[EffectMethod]
|
||||
public async Task HandleGetAllLdapUsers(GetAllLdapUsers action, IDispatcher dispatcher)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,28 @@ namespace Wollmilchsau.Store.AdminStore;
|
||||
|
||||
public static class AdminReducers
|
||||
{
|
||||
[ReducerMethod]
|
||||
public static AdminState ReduceSetMoodleUsers(AdminState state, SetMoodleUsers action)
|
||||
{
|
||||
return state with
|
||||
{
|
||||
MoodleUsers = action.Users.ToImmutableDictionary(user => user.Id),
|
||||
MoodleUsersLoaded = true,
|
||||
MoodleUsersLoadFailed = false
|
||||
};
|
||||
}
|
||||
|
||||
[ReducerMethod]
|
||||
public static AdminState ReduceSetMoodleUsersLoadFailed(AdminState state, SetMoodleUsersLoadFailed action)
|
||||
{
|
||||
return state with
|
||||
{
|
||||
MoodleUsers = [],
|
||||
MoodleUsersLoaded = true,
|
||||
MoodleUsersLoadFailed = true
|
||||
};
|
||||
}
|
||||
|
||||
[ReducerMethod]
|
||||
public static AdminState ReduceSetLdapUsers(AdminState state, SetLdapUsers action)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,9 @@ public sealed record AdminState
|
||||
{
|
||||
public AdminState() { }
|
||||
public ImmutableDictionary<Guid, ElementUser> ElementUsers { get; init; } = [];
|
||||
public ImmutableDictionary<Guid, MoodleUser> MoodleUsers { get; init; } = [];
|
||||
public ImmutableDictionary<int, MoodleUser> MoodleUsers { get; init; } = [];
|
||||
public bool MoodleUsersLoaded { get; init; }
|
||||
public bool MoodleUsersLoadFailed { get; init; }
|
||||
public ImmutableDictionary<string, LdapUser> LdapUsers { get; init; } = [];
|
||||
public ImmutableList<Classroom> Classrooms { get; init; } = [];
|
||||
public bool LdapUsersLoaded { get; init; }
|
||||
|
||||
Reference in New Issue
Block a user