Wired in the Office subscription page

This commit is contained in:
Rhywden
2026-08-30 18:10:37 +02:00
parent 78656d7165
commit 04f7f527be
16 changed files with 627 additions and 10 deletions
+22 -10
View File
@@ -6,16 +6,28 @@
<AuthorizeView Roles="ROLE_STUDENT,ROLE_TEACHER,ROLE_ADMIN">
<Authorized>
<MudContainer>
<MudCard Style="width: 300px;">
<MudCardMedia Image="images/questionnaire.svg" Height="200" Style="width: 300px;" />
<MudCardContent>
<MudText Typo="Typo.h5">Umfragen</MudText>
<MudText Typo="Typo.body2">Hier kannst du an Umfragen teilnehmen.</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/students/questionaire">Umfragen</MudButton>
</MudCardActions>
</MudCard>
<MudStack Row="true" Spacing="10">
<MudCard Style="width: 300px;">
<MudCardMedia Image="images/office.png" Height="200" Style="width: 300px;" />
<MudCardContent>
<MudText Typo="Typo.h5">Office Anmeldung</MudText>
<MudText Typo="Typo.body2">Hier kannst du dich für Office anmelden.</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/office/userhome">Office Anmeldung</MudButton>
</MudCardActions>
</MudCard>
<MudCard Style="width: 300px;">
<MudCardMedia Image="images/questionnaire.svg" Height="200" Style="width: 300px;" />
<MudCardContent>
<MudText Typo="Typo.h5">Umfragen</MudText>
<MudText Typo="Typo.body2">Hier kannst du an Umfragen teilnehmen.</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/students/questionaire">Umfragen</MudButton>
</MudCardActions>
</MudCard>
</MudStack>
</MudContainer>
</Authorized>
</AuthorizeView>
@@ -0,0 +1,172 @@
@page "/office/userhome"
@using Fluxor
@using Microsoft.AspNetCore.Authorization
@using Wollmilchsau.Store.OfficeStore
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IDispatcher Dispatcher
@inject IState<OfficeState> OfficeState
@attribute [Authorize]
<MudPaper Class="d-flex justify-center" Elevation="0">
@if (OfficeState.Value?.HasLoaded.GetValueOrDefault(CurrentUser?.Username ?? string.Empty) == true)
{
<MudCard Style="width: 800px;">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Dein Office-Account</MudText>
</CardHeaderContent>
</MudCardHeader>
@if (GraphUser is not null)
{
<MudCardContent>
<MudText>Dein Username: @GraphUser.UserPrincipalName</MudText>
<MudText>Herunterladen unter: <MudLink Href="https://office.com">https://office.com</MudLink></MudText>
@if (OfficeState.Value?.NewPasswords.GetValueOrDefault(CurrentUser?.Username ?? string.Empty) is string newPassword)
{
<MudText>Dein temporäres Passwort: <b>@newPassword</b></MudText>
<MudAlert Severity="Severity.Warning" Elevation="0" Variant="Variant.Filled">
Du wirst beim ersten Anmelden aufgefordert, dein Passwort zu ändern. Notiere unbedingt dieses neue Passwort!
</MudAlert>
}
else
{
<MudText>Wenn du dein Passwort nicht mehr kennst, kannst du es über den untenstehenden Button zurücksetzen.</MudText>
}
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ResetPassword">Passwort zurücksetzen</MudButton>
</MudCardActions>
}
else
{
<MudCardContent>
<MudText>Derzeit existiert anscheinend kein Office-Account unter @CurrentUser?.Username&#64;bs18.de</MudText>
<MudText>Sollte hier ein Fehler vorliegen bitte an <MudLink Href="@MailtoContactEmail">@ContactEmail</MudLink> schreiben.</MudText>
<MudText>Ansonsten kannst Du über den untenstehenden Button einen Account erstellen.</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="CreateAccount">Account erstellen</MudButton>
</MudCardActions>
}
</MudCard>
}
else
{
<MudCard Style="max-width: 800px;">
<MudCardHeader>
<CardHeaderContent>
<MudSkeleton Width="200px" Height="30px" />
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSkeleton Width="100%" Height="20px" Class="mb-2" />
<MudSkeleton Width="100%" Height="20px" Class="mb-2" />
<MudSkeleton Width="100%" Height="20px" Class="mb-2" />
</MudCardContent>
<MudCardActions>
<MudSkeleton Width="150px" Height="36px" />
</MudCardActions>
</MudCard>
}
</MudPaper>
<MudPaper Class="d-flex justify-center" Elevation="0">
<MudExpansionPanels MultiExpansion="true" Style="width: 800px;">
<MudExpansionPanel Text="Informationen" Expanded>
<MudText>Um das Office-Paket zu nutzen, bestehen mehrere Möglichkeiten:</MudText>
<MudText>1. Nutzen der Webanwendungen über <MudLink Href="https://office.com" Target="_blank">https://office.com</MudLink></MudText>
<MudText>2. Installieren der Officeanwendungen auf dem eigenen Gerät. Siehe Anleitungen unten.</MudText>
</MudExpansionPanel>
<MudExpansionPanel Text="Windows">
<MudText>
Sollte das Office-Paket nicht vorinstalliert sein, so lässt sich Office auch manuell installieren.
Dafür bei <MudLink Href="https://m365.cloud.microsoft/apps">MS Office Website</MudLink> anmelden und rechts oben auf "Apps Installieren" clicken.
Die erste Option "Microsoft 365 Apps" ist dann die richtige.
</MudText>
</MudExpansionPanel>
<MudExpansionPanel Text="MacOS">
<MudText>Für MacOS lässt sich das Officepaket direkt aus dem Mac App Store herunterladen:</MudText>
<MudLink Href="https://apps.apple.com/de/app-bundle/microsoft-365/id1450038993?mt=12" Target="_blank">Microsoft 365 im Mac App Store</MudLink>
<MudText>
Alternativ lässt sich Office auch manuell installieren. Dafür bei <MudLink Href="https://m365.cloud.microsoft/apps">MS Office Website</MudLink> anmelden und rechts oben auf "Apps Installieren" clicken.
Die erste Option "Microsoft 365 Apps" ist dann die richtige.
</MudText>
</MudExpansionPanel>
<MudExpansionPanel Text="iPhone & iPad">
<MudText>Um Office auf iPad und iPhone nutzen zu können, musst du jede Anwendung einzeln herunterladen.</MudText>
<MudStack>
<MudStack Row Class="d-flex align-center">
<MudImage Src="images/wordqr.png" Width="200" />
<MudLink Href="https://apps.apple.com/de/app/microsoft-word/id586447913" Target="_blank">Microsoft Word</MudLink>
</MudStack>
<MudStack Row Class="d-flex align-center">
<MudImage Src="images/excelqr.png" Width="200" />
<MudLink Href="https://apps.apple.com/de/app/microsoft-excel/id586683407" Target="_blank">Microsoft Excel</MudLink>
</MudStack>
<MudStack Row Class="d-flex align-center">
<MudImage Src="images/powerpointqr.png" Width="200" />
<MudLink Href="https://apps.apple.com/de/app/microsoft-powerpoint/id586449534" Target="_blank">Microsoft PowerPoint</MudLink>
</MudStack>
</MudStack>
</MudExpansionPanel>
<MudExpansionPanel Text="Android / GooglePlayStore">
<MudStack>
<MudStack Row Class="d-flex align-center">
<MudImage Src="images/googlewordqr.png" Width="200" />
<MudLink Href="https://play.google.com/store/apps/details?id=com.microsoft.office.word&hl=de" Target="_blank">Microsoft Word</MudLink>
</MudStack>
<MudStack Row Class="d-flex align-center">
<MudImage Src="images/googleexcelqr.png" Width="200" />
<MudLink Href="https://play.google.com/store/apps/details?id=com.microsoft.office.excel&hl=de" Target="_blank">Microsoft Excel</MudLink>
</MudStack>
<MudStack Row Class="d-flex align-center">
<MudImage Src="images/googlepptqr.png" Width="200" />
<MudLink Href="https://play.google.com/store/apps/details?id=com.microsoft.office.powerpoint&hl=de" Target="_blank">Microsoft PowerPoint</MudLink>
</MudStack>
</MudStack>
</MudExpansionPanel>
</MudExpansionPanels>
</MudPaper>
@code {
[CascadingParameter] private Task<AuthenticationState>? AuthenticationStateTask { get; set; }
AuthenticationState? _authenticationState;
Microsoft.Graph.Models.User? GraphUser => OfficeState.Value.GraphUsers.GetValueOrDefault(CurrentUser?.Username ?? string.Empty);
Models.LdapUser? CurrentUser;
string? CurrentUserName;
string ContactEmail = "johannes.hass@bs18.de";
string MailtoContactEmail => $"mailto:{ContactEmail}";
override protected async Task OnInitializedAsync()
{
if (AuthenticationStateTask is not null)
{
_authenticationState = await AuthenticationStateTask;
CurrentUserName = _authenticationState.User.FindFirst("preferred_username")?.Value;
CurrentUser = new Models.LdapUser(
CurrentUserName ?? string.Empty,
_authenticationState.User.FindFirst("family_name")?.Value ?? string.Empty,
_authenticationState.User.FindFirst("classroom")?.Value ?? string.Empty,
_authenticationState.User.FindFirst("given_name")?.Value ?? string.Empty,
false,
_authenticationState.User.IsInRole("ROLE_TEACHER") ? Models.LdapUserRole.Teacher : Models.LdapUserRole.Student
);
Dispatcher.Dispatch(new GetCurrentUserInfoAction(CurrentUserName));
}
await base.OnInitializedAsync();
}
private void CreateAccount(MouseEventArgs args)
{
if (CurrentUser is null) return;
Dispatcher.Dispatch(new CreateNewAccountAction(CurrentUser));
}
private void ResetPassword(MouseEventArgs args)
{
if (CurrentUserName is null || GraphUser is null || GraphUser.UserPrincipalName is null) return;
Dispatcher.Dispatch(new SetNewPasswordInGraphAction(CurrentUserName, GraphUser.UserPrincipalName));
}
}
+56
View File
@@ -0,0 +1,56 @@
using System.Security.Cryptography;
namespace Wollmilchsau.Helpers;
internal static class PasswordGenerator
{
// Avoid ambiguous: l, I, O, 0, 1 (and also exclude o for extra safety)
private const string Lower = "abcdefghijkmnpqrstuvwxyz"; // no l, no o
private const string Upper = "ABCDEFGHJKMNPQRSTUVWXYZ"; // no I, no O
private const string Digits = "23456789"; // no 0, no 1
private const string Specials = "#!?$";
internal static string Create(
int length = 16,
int minLower = 1,
int minUpper = 1,
int minDigits = 1,
int minSpecial = 1)
{
if (length < minLower + minUpper + minDigits + minSpecial)
{
throw new ArgumentOutOfRangeException(nameof(length), "Length too short for required character counts.");
}
Span<char> buffer = length <= 256 ? stackalloc char[length] : new char[length];
var i = 0;
Fill(buffer, ref i, Lower, minLower);
Fill(buffer, ref i, Upper, minUpper);
Fill(buffer, ref i, Digits, minDigits);
Fill(buffer, ref i, Specials, minSpecial);
var all = Lower + Upper + Digits + Specials;
Fill(buffer, ref i, all, length - i);
Shuffle(buffer);
return new string(buffer);
}
private static void Fill(Span<char> target, ref int index, string alphabet, int count)
{
for (var c = 0; c < count; c++)
{
target[index++] = alphabet[RandomNumberGenerator.GetInt32(alphabet.Length)];
}
}
private static void Shuffle(Span<char> chars)
{
for (var n = chars.Length - 1; n > 0; n--)
{
var k = RandomNumberGenerator.GetInt32(n + 1);
(chars[n], chars[k]) = (chars[k], chars[n]);
}
}
}
+4
View File
@@ -13,6 +13,7 @@ using Wollmilchsau.Components;
using Wollmilchsau.Exceptions;
using Wollmilchsau.Services.Ldap;
using Wollmilchsau.Services.Moodle;
using Wollmilchsau.Services.Office;
var builder = WebApplication.CreateBuilder(args);
@@ -95,6 +96,9 @@ builder.Services.AddOptions<MoodleOptions>()
.BindConfiguration("Moodle")
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IOfficeService, OfficeService>();
builder.Services.AddHttpClient<IMoodleService, MoodleService>((serviceProvider, client) =>
{
var options = serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<MoodleOptions>>().Value;
@@ -0,0 +1,247 @@
using Azure.Core;
using Azure.Identity;
using Microsoft.Kiota.Abstractions;
using Microsoft.Graph;
using Microsoft.Graph.Models;
namespace Wollmilchsau.Services.Office;
public interface IOfficeService
{
IAsyncEnumerable<User> GetAllUsersAsync(CancellationToken cancellationToken = default);
Task<User?> GetUserByMailAsync(string mailAddress, CancellationToken cancellationToken = default);
Task<User?> GetUserByAliasesAsync(string username, CancellationToken cancellationToken = default);
Task<bool> DeleteUserByIdAsync(string userId, CancellationToken cancellationToken = default);
Task<User> CreateUserAsync(
string displayName,
string userPrincipalName,
string mailNickname,
string initialPassword,
string givenName,
string surname,
string? department = null,
string? jobTitle = null,
string usageLocation = "DE",
CancellationToken cancellationToken = default);
Task<bool> ChangeUserPasswordAsync(
string userId,
string newPassword,
bool forceChangePasswordNextSignIn = true,
CancellationToken cancellationToken = default);
}
public class OfficeService : IOfficeService
{
private static readonly string[] Scopes = ["https://graph.microsoft.com/.default"];
private GraphServiceClient _client { get; set; }
public OfficeService(IConfiguration configuration)
{
var tenantId = configuration["MSGraph:TenantId"]!;
var clientId = configuration["MSGraph:ClientId"]!;
var clientSecret = configuration["MSGraph:ClientSecret"]!;
TokenCredential credential = new ClientSecretCredential(
tenantId: tenantId,
clientId: clientId,
clientSecret: clientSecret);
_client = new GraphServiceClient(credential, Scopes);
}
public async IAsyncEnumerable<User> GetAllUsersAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var page = await _client.Users.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Select = ["id", "displayName", "userPrincipalName", "mail", "department", "jobTitle", "proxyAddresses"];
requestConfiguration.QueryParameters.Top = 999;
}, cancellationToken);
while (page?.Value is { Count: > 0 } value)
{
foreach (var user in value)
{
cancellationToken.ThrowIfCancellationRequested();
yield return user;
}
if (string.IsNullOrWhiteSpace(page.OdataNextLink))
{
break;
}
page = await _client.Users
.WithUrl(page.OdataNextLink)
.GetAsync(cancellationToken: cancellationToken);
}
}
public async Task<User?> GetUserByMailAsync(string mailAddress, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(mailAddress))
{
throw new ArgumentException("Mail address must be provided.", nameof(mailAddress));
}
// Use advanced query capabilities for filtering.
// Note: Graph user filtering is case-insensitive for many string properties, but callers should not rely on it.
var response = await _client.Users.GetAsync(requestConfiguration =>
{
requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");
requestConfiguration.QueryParameters.Count = true;
requestConfiguration.QueryParameters.Top = 1;
requestConfiguration.QueryParameters.Select = [
"id",
"displayName",
"userPrincipalName",
"mail",
"department",
"jobTitle",
"proxyAddresses"
];
requestConfiguration.QueryParameters.Filter = $"mail eq '{mailAddress.Replace("'", "''")}'";
}, cancellationToken);
return response?.Value?.FirstOrDefault();
}
public async Task<User?> GetUserByAliasesAsync(string userid, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(userid))
{
throw new ArgumentException("Mail address must be provided.", nameof(userid));
}
// Use advanced query capabilities for filtering.
// Note: Graph user filtering is case-insensitive for many string properties, but callers should not rely on it.
var response = await _client.Users.GetAsync(requestConfiguration =>
{
requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");
requestConfiguration.QueryParameters.Count = true;
requestConfiguration.QueryParameters.Top = 1;
requestConfiguration.QueryParameters.Select = [
"id",
"displayName",
"userPrincipalName",
"mail",
"department",
"jobTitle",
"proxyAddresses"
];
requestConfiguration.QueryParameters.Filter = $"proxyAddresses/any(x:startswith(x,'smtp:{userid}@')) or proxyAddresses/any(x:startswith(x,'SMTP:{userid}@'))";
}, cancellationToken);
return response?.Value?.FirstOrDefault();
}
public async Task<bool> DeleteUserByIdAsync(string userId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(userId))
{
throw new ArgumentException("User id must be provided.", nameof(userId));
}
try
{
await _client.Users[userId].DeleteAsync(cancellationToken: cancellationToken);
return true;
}
catch (ApiException ex) when (ex.ResponseStatusCode == (int)System.Net.HttpStatusCode.NotFound)
{
return false;
}
}
public async Task<User> CreateUserAsync(
string displayName,
string userPrincipalName,
string mailNickname,
string initialPassword,
string givenName,
string surname,
string? department = null,
string? jobTitle = null,
string usageLocation = "DE",
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(displayName))
{
throw new ArgumentException("Display name must be provided.", nameof(displayName));
}
if (string.IsNullOrWhiteSpace(userPrincipalName))
{
throw new ArgumentException("User principal name must be provided.", nameof(userPrincipalName));
}
if (string.IsNullOrWhiteSpace(mailNickname))
{
throw new ArgumentException("Mail nickname must be provided.", nameof(mailNickname));
}
if (string.IsNullOrWhiteSpace(initialPassword))
{
throw new ArgumentException("Initial password must be provided.", nameof(initialPassword));
}
if (string.IsNullOrWhiteSpace(usageLocation))
{
throw new ArgumentException("Usage location must be provided.", nameof(usageLocation));
}
var user = new User
{
AccountEnabled = true,
DisplayName = displayName,
Department = department,
JobTitle = jobTitle,
MailNickname = mailNickname,
UserPrincipalName = userPrincipalName,
UsageLocation = usageLocation,
GivenName = givenName,
Surname = surname,
PasswordProfile = new PasswordProfile
{
Password = initialPassword,
ForceChangePasswordNextSignIn = true
}
};
return await _client.Users.PostAsync(user, cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Microsoft Graph returned no user after creation.");
}
public async Task<bool> ChangeUserPasswordAsync(
string userId,
string newPassword,
bool forceChangePasswordNextSignIn = true,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(userId))
{
throw new ArgumentException("User id must be provided.", nameof(userId));
}
if (string.IsNullOrWhiteSpace(newPassword))
{
throw new ArgumentException("New password must be provided.", nameof(newPassword));
}
var userUpdate = new User
{
PasswordProfile = new PasswordProfile
{
Password = newPassword,
ForceChangePasswordNextSignIn = forceChangePasswordNextSignIn
}
};
try
{
await _client.Users[userId].PatchAsync(userUpdate, cancellationToken: cancellationToken);
return true;
}
catch (ApiException ex) when (ex.ResponseStatusCode == (int)System.Net.HttpStatusCode.NotFound)
{
return false;
}
}
}
@@ -0,0 +1,10 @@
using Microsoft.Graph.Models;
namespace Wollmilchsau.Store.OfficeStore;
public sealed record GetCurrentUserInfoAction(string? UserId);
public sealed record UpdateCurrentUserInfoWithResultAction(string UserId, User? GraphUser, Models.LdapUser? LdapUser);
public sealed record CreateNewAccountAction(Models.LdapUser NewUser);
public sealed record SetNewlyCreatedGraphUserAction(string UserId, User GraphUser, string Password);
public sealed record SetNewPasswordInGraphAction(string UserId, string UserPrincipalName);
public sealed record SetNewPasswordInStoreAction(string UserId, string NewPassword);
@@ -0,0 +1,51 @@
using Fluxor;
using Wollmilchsau.Helpers;
using Wollmilchsau.Services;
using Wollmilchsau.Services.Ldap;
using Wollmilchsau.Services.Office;
namespace Wollmilchsau.Store.OfficeStore;
public sealed class OfficeEffects(IOfficeService officeService, ILdapService ldapService)
{
[EffectMethod]
public async Task HandleGetCurrentUserInfoAction(GetCurrentUserInfoAction action, IDispatcher dispatcher)
{
if (action.UserId is null) return;
var graphUser = await officeService.GetUserByAliasesAsync(action.UserId);
//var ldapUser = await ldapService.GetUserByUidAsync(action.UserId);
dispatcher.Dispatch(new UpdateCurrentUserInfoWithResultAction(action.UserId, graphUser, /*ldapUser.ValueOrDefault*/ null));
}
[EffectMethod]
public async Task HandleCreateNewAccountAction(CreateNewAccountAction action, IDispatcher dispatcher)
{
var initialPassword = PasswordGenerator.Create(length: 10);
var createdUser = await officeService.CreateUserAsync(
displayName: $"{action.NewUser.GivenName} {action.NewUser.Surname}",
userPrincipalName: $"{action.NewUser.Username}@bs18.de",
mailNickname: action.NewUser.Username,
initialPassword: initialPassword,
givenName: action.NewUser.GivenName ?? string.Empty,
surname: action.NewUser.Surname ?? string.Empty,
department: action.NewUser.Role == Models.LdapUserRole.Teacher ? "Lehrer" : "Schüler",
jobTitle: action.NewUser.Classroom ?? "n/a"
);
dispatcher.Dispatch(new SetNewlyCreatedGraphUserAction(
action.NewUser.Username,
createdUser,
initialPassword));
}
[EffectMethod]
public async Task HandleSetNewPasswordInGraphAction(SetNewPasswordInGraphAction action, IDispatcher dispatcher)
{
var initialPassword = PasswordGenerator.Create(length: 10);
var success = await officeService.ChangeUserPasswordAsync(
userId: action.UserPrincipalName, newPassword: initialPassword);
if (success)
{
dispatcher.Dispatch(new SetNewPasswordInStoreAction(action.UserId, initialPassword));
}
}
}
@@ -0,0 +1,51 @@
using Fluxor;
namespace Wollmilchsau.Store.OfficeStore;
public static class OfficeReducers
{
[ReducerMethod]
public static OfficeState ReduceUpdateCurrentUserInfoWithResultAction(OfficeState state, UpdateCurrentUserInfoWithResultAction action)
{
var ldapUsers = state.LdapUsers;
var graphUsers = state.GraphUsers;
var hasLoaded = state.HasLoaded;
if (action.LdapUser is not null)
{
ldapUsers = ldapUsers.SetItem(action.UserId, action.LdapUser);
}
if (action.GraphUser is not null)
{
graphUsers = graphUsers.SetItem(action.UserId, action.GraphUser);
}
return state with
{
LdapUsers = ldapUsers,
GraphUsers = graphUsers,
HasLoaded = hasLoaded.SetItem(action.UserId, true)
};
}
[ReducerMethod]
public static OfficeState ReduceSetNewPasswordInStoreAction(OfficeState state, SetNewPasswordInStoreAction action)
{
var newPasswords = state.NewPasswords;
newPasswords = newPasswords.SetItem(action.UserId, action.NewPassword);
return state with
{
NewPasswords = newPasswords
};
}
[ReducerMethod]
public static OfficeState ReduceSetNewlyCreatedGraphUserAction(OfficeState state, SetNewlyCreatedGraphUserAction action)
{
var graphUsers = state.GraphUsers;
graphUsers = graphUsers.SetItem(action.UserId, action.GraphUser);
var newPasswords = state.NewPasswords;
newPasswords = newPasswords.SetItem(action.UserId, action.Password);
return state with
{
GraphUsers = graphUsers,
NewPasswords = newPasswords
};
}
}
@@ -0,0 +1,14 @@
using Fluxor;
using System.Collections.Immutable;
namespace Wollmilchsau.Store.OfficeStore;
[FeatureState]
public sealed record OfficeState
{
public OfficeState() { }
public ImmutableDictionary<string, Microsoft.Graph.Models.User> GraphUsers { get; init; } = [];
public ImmutableDictionary<string, Models.LdapUser> LdapUsers { get; init; } = [];
public ImmutableDictionary<string, bool> HasLoaded { get; init; } = [];
public ImmutableDictionary<string, string> NewPasswords { get; init; } = [];
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB