Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/ServiceDefaults/ServiceDefaults.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />

<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.2.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.2.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.5.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.5.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
</ItemGroup>
Expand Down
217 changes: 116 additions & 101 deletions src/Web/Features/UserManagement/UserManagementHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,115 +8,130 @@
//=======================================================

using Auth0.ManagementApi;
using Auth0.ManagementApi.Models;
using Auth0.ManagementApi.Paging;
using Auth0.ManagementApi.Users;

namespace MyBlog.Web.Features.UserManagement;

public sealed class UserManagementHandler(
IConfiguration configuration,
IHttpClientFactory httpClientFactory)
: IRequestHandler<GetUsersWithRolesQuery, Result<IReadOnlyList<UserWithRolesDto>>>,
IRequestHandler<AssignRoleCommand, Result>,
IRequestHandler<RemoveRoleCommand, Result>,
IRequestHandler<GetAvailableRolesQuery, Result<IReadOnlyList<RoleDto>>>
{
public async Task<Result<IReadOnlyList<UserWithRolesDto>>> Handle(
GetUsersWithRolesQuery request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
var users = await client.Users.GetAllAsync(new GetUsersRequest(), new PaginationInfo(), ct);
var result = new List<UserWithRolesDto>();
foreach (var user in users)
{
var roles = await client.Users.GetRolesAsync(user.UserId, new PaginationInfo(), ct);
result.Add(new UserWithRolesDto(
user.UserId,
user.Email ?? string.Empty,
user.FullName ?? user.Email ?? string.Empty,
roles.Select(r => r.Name ?? string.Empty).ToList()));
}
return Result.Ok<IReadOnlyList<UserWithRolesDto>>(result);
}
catch (Exception ex)
{
return Result.Fail<IReadOnlyList<UserWithRolesDto>>(ex.Message);
}
}
IConfiguration configuration,
IHttpClientFactory httpClientFactory)
: IRequestHandler<GetUsersWithRolesQuery, Result<IReadOnlyList<UserWithRolesDto>>>,
IRequestHandler<AssignRoleCommand, Result>,
IRequestHandler<RemoveRoleCommand, Result>,
IRequestHandler<GetAvailableRolesQuery, Result<IReadOnlyList<RoleDto>>>
{
public async Task<Result<IReadOnlyList<UserWithRolesDto>>> Handle(
GetUsersWithRolesQuery request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
var usersPager = await client.Users.ListAsync(new ListUsersRequestParameters(), cancellationToken: ct);
var result = new List<UserWithRolesDto>();
await foreach (var user in usersPager)
{
var rolesPager = await client.Users.Roles.ListAsync(
user.UserId ?? string.Empty, new ListUserRolesRequestParameters(), cancellationToken: ct);
var roles = new List<string>();
await foreach (var role in rolesPager)
{
roles.Add(role.Name ?? string.Empty);
}
result.Add(new UserWithRolesDto(
user.UserId ?? string.Empty,
user.Email ?? string.Empty,
user.Name ?? user.Email ?? string.Empty,
roles));
}
return Result.Ok<IReadOnlyList<UserWithRolesDto>>(result);
}
catch (Exception ex)
{
return Result.Fail<IReadOnlyList<UserWithRolesDto>>(ex.Message);
}
}

public async Task<Result> Handle(AssignRoleCommand request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
await client.Users.AssignRolesAsync(request.UserId,
new AssignRolesRequest { Roles = [request.RoleId] }, ct);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}
public async Task<Result> Handle(AssignRoleCommand request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
await client.Users.Roles.AssignAsync(
request.UserId,
new AssignUserRolesRequestContent { Roles = [request.RoleId] },
cancellationToken: ct);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}

public async Task<Result> Handle(RemoveRoleCommand request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
await client.Users.RemoveRolesAsync(request.UserId,
new AssignRolesRequest { Roles = [request.RoleId] }, ct);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}
public async Task<Result> Handle(RemoveRoleCommand request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
await client.Users.Roles.DeleteAsync(
request.UserId,
new DeleteUserRolesRequestContent { Roles = [request.RoleId] },
cancellationToken: ct);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}

public async Task<Result<IReadOnlyList<RoleDto>>> Handle(GetAvailableRolesQuery request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
var roles = await client.Roles.GetAllAsync(new GetRolesRequest(), new PaginationInfo(), ct);
return Result.Ok<IReadOnlyList<RoleDto>>(
roles.Select(r => new RoleDto(r.Id ?? string.Empty, r.Name ?? string.Empty)).ToList());
}
catch (Exception ex)
{
return Result.Fail<IReadOnlyList<RoleDto>>(ex.Message);
}
}
public async Task<Result<IReadOnlyList<RoleDto>>> Handle(GetAvailableRolesQuery request, CancellationToken ct)
{
try
{
var client = await GetManagementClientAsync(ct);
var rolesPager = await client.Roles.ListAsync(new ListRolesRequestParameters(), cancellationToken: ct);
var roles = new List<RoleDto>();
await foreach (var role in rolesPager)
{
roles.Add(new RoleDto(role.Id ?? string.Empty, role.Name ?? string.Empty));
}
return Result.Ok<IReadOnlyList<RoleDto>>(roles);
}
catch (Exception ex)
{
return Result.Fail<IReadOnlyList<RoleDto>>(ex.Message);
}
}

private async Task<ManagementApiClient> GetManagementClientAsync(CancellationToken ct)
{
var domain = configuration["Auth0:ManagementApiDomain"]
?? throw new InvalidOperationException("Auth0:ManagementApiDomain not configured.");
var clientId = configuration["Auth0:ManagementApiClientId"]
?? throw new InvalidOperationException("Auth0:ManagementApiClientId not configured.");
var clientSecret = configuration["Auth0:ManagementApiClientSecret"]
?? throw new InvalidOperationException("Auth0:ManagementApiClientSecret not configured.");
private async Task<ManagementApiClient> GetManagementClientAsync(CancellationToken ct)
{
var domain = configuration["Auth0:ManagementApiDomain"]
?? throw new InvalidOperationException("Auth0:ManagementApiDomain not configured.");
var clientId = configuration["Auth0:ManagementApiClientId"]
?? throw new InvalidOperationException("Auth0:ManagementApiClientId not configured.");
var clientSecret = configuration["Auth0:ManagementApiClientSecret"]
?? throw new InvalidOperationException("Auth0:ManagementApiClientSecret not configured.");

var httpClient = httpClientFactory.CreateClient();
var tokenResponse = await httpClient.PostAsJsonAsync(
$"https://{domain}/oauth/token",
new
{
client_id = clientId,
client_secret = clientSecret,
audience = $"https://{domain}/api/v2/",
grant_type = "client_credentials"
}, ct);
tokenResponse.EnsureSuccessStatusCode();
var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>(ct);
return new ManagementApiClient(tokenData!.AccessToken, domain);
}
var httpClient = httpClientFactory.CreateClient();
var tokenResponse = await httpClient.PostAsJsonAsync(
$"https://{domain}/oauth/token",
new
{
client_id = clientId,
client_secret = clientSecret,
audience = $"https://{domain}/api/v2/",
grant_type = "client_credentials"
}, ct);
tokenResponse.EnsureSuccessStatusCode();
var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>(ct);
return new ManagementApiClient(
token: tokenData!.AccessToken,
clientOptions: new ClientOptions { BaseUrl = $"https://{domain}/api/v2" });
}

private sealed class TokenResponse
{
public string AccessToken { get; init; } = string.Empty;
}
private sealed class TokenResponse
{
public string AccessToken { get; init; } = string.Empty;
}
}
3 changes: 2 additions & 1 deletion src/Web/Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
<PackageReference Include="Aspire.MongoDB.Driver" Version="13.2.2" />
<PackageReference Include="Aspire.StackExchange.Redis.DistributedCaching" Version="13.2.2" />
<PackageReference Include="Auth0.AspNetCore.Authentication" Version="1.7.0" />
<PackageReference Include="Auth0.ManagementApi" Version="7.46.0" />
<PackageReference Include="Auth0.ManagementApi" Version="8.1.0" />
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.6" />
<PackageReference Include="MongoDB.EntityFrameworkCore" Version="10.0.1" />
</ItemGroup>

Expand Down
6 changes: 3 additions & 3 deletions tests/Architecture.Tests/Architecture.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="FluentAssertions" Version="8.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="NetArchTest.Rules" Version="1.3.2" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions tests/Integration.Tests/Integration.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="13.2.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="FluentAssertions" Version="8.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="4.11.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions tests/Unit.Tests/Unit.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@

<ItemGroup>
<PackageReference Include="bunit" Version="2.7.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="coverlet.msbuild" Version="10.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="8.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading