Wired in the Office subscription page
@@ -6,6 +6,17 @@
|
||||
<AuthorizeView Roles="ROLE_STUDENT,ROLE_TEACHER,ROLE_ADMIN">
|
||||
<Authorized>
|
||||
<MudContainer>
|
||||
<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>
|
||||
@@ -16,6 +27,7 @@
|
||||
<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@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));
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |