Skip to content

Commit 0bb30cc

Browse files
merge pull request #21 from https-richardy/feature/20-multiple-clients-support
[#20] - implements multiple clients support
2 parents 1e03baf + 96cd3b4 commit 0bb30cc

79 files changed

Lines changed: 2908 additions & 218 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Applications/Backend/Source/HttpsRichardy.Federation.Application/Handlers/Authorization/AuthorizationHandler.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
namespace HttpsRichardy.Federation.Application.Handlers.Authorization;
22

3-
public sealed class AuthorizationHandler(IRealmCollection realmCollection, IRedirectUriPolicy redirectUriPolicy) :
3+
public sealed class AuthorizationHandler(IClientCollection clientCollection, IRedirectUriPolicy redirectUriPolicy) :
44
IDispatchHandler<AuthorizationParameters, Result<AuthorizationScheme>>
55
{
66
public async Task<Result<AuthorizationScheme>> HandleAsync(
77
AuthorizationParameters parameters, CancellationToken cancellation = default)
88
{
9-
var filters = new RealmFiltersBuilder()
10-
.WithClientId(parameters.ClientId)
9+
var filters = ClientFilters.WithSpecifications()
10+
.WithIdentifier(parameters.ClientId)
1111
.Build();
1212

13-
var clients = await realmCollection.GetRealmsAsync(filters, cancellation);
13+
var clients = await clientCollection.GetClientsAsync(filters, cancellation);
1414
var client = clients.FirstOrDefault();
1515

1616
if (client is null)

Applications/Backend/Source/HttpsRichardy.Federation.Application/Handlers/Authorization/ClientCredentialsGrantHandler.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
namespace HttpsRichardy.Federation.Application.Handlers.Authorization;
22

3-
public sealed class ClientCredentialsGrantHandler(IRealmCollection realmCollection, ISecurityTokenService tokenService) :
3+
public sealed class ClientCredentialsGrantHandler(IClientCollection clientCollection, ISecurityTokenService tokenService) :
44
IAuthorizationFlowHandler
55
{
66
public Grant Grant => Grant.ClientCredentials;
77

88
public async Task<Result<ClientAuthenticationResult>> HandleAsync(ClientAuthenticationCredentials parameters, CancellationToken cancellation = default)
99
{
10-
var filters = new RealmFiltersBuilder()
10+
var filters = ClientFilters.WithSpecifications()
1111
.WithClientId(parameters.ClientId)
1212
.Build();
1313

14-
var realms = await realmCollection.GetRealmsAsync(filters, cancellation: cancellation);
15-
var realm = realms.FirstOrDefault();
14+
var clients = await clientCollection.GetClientsAsync(filters, cancellation: cancellation);
15+
var client = clients.FirstOrDefault();
1616

17-
if (realm is null)
17+
if (client is null)
1818
{
1919
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.ClientNotFound);
2020
}
2121

22-
if (parameters.ClientSecret != realm.SecretHash)
22+
if (parameters.ClientSecret != client.Secret)
2323
{
2424
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.InvalidClientCredentials);
2525
}
2626

27-
var tokenResult = await tokenService.GenerateAccessTokenAsync(realm, cancellation);
27+
var tokenResult = await tokenService.GenerateAccessTokenAsync(client, cancellation);
2828
if (tokenResult.IsFailure)
2929
{
3030
return Result<ClientAuthenticationResult>.Failure(tokenResult.Error);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class AssignAudienceClientHandler(IClientCollection clientCollection) :
4+
IDispatchHandler<AssignClientAudienceScheme, Result<IReadOnlyCollection<string>>>
5+
{
6+
public async Task<Result<IReadOnlyCollection<string>>> HandleAsync(
7+
AssignClientAudienceScheme parameters, CancellationToken cancellation = default)
8+
{
9+
var filters = ClientFilters.WithSpecifications()
10+
.WithIdentifier(parameters.Id)
11+
.Build();
12+
13+
var clients = await clientCollection.GetClientsAsync(filters, cancellation);
14+
var client = clients.FirstOrDefault();
15+
16+
if (client is null)
17+
{
18+
return Result<IReadOnlyCollection<string>>.Failure(ClientErrors.ClientDoesNotExist);
19+
}
20+
21+
var audience = new Audience(parameters.Value);
22+
if (client.Audiences.Any(current => current.Value == audience.Value))
23+
{
24+
return Result<IReadOnlyCollection<string>>.Failure(ClientErrors.ClientAlreadyHasAudience);
25+
}
26+
27+
client.Audiences.Add(audience);
28+
29+
await clientCollection.UpdateAsync(client, cancellation);
30+
31+
return Result<IReadOnlyCollection<string>>.Success([.. client.Audiences.Select(current => current.Value)]);
32+
}
33+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class AssignPermissionClientHandler(IClientCollection clientCollection, IPermissionCollection permissionCollection) :
4+
IDispatchHandler<AssignClientPermissionScheme, Result<IReadOnlyCollection<PermissionDetailsScheme>>>
5+
{
6+
public async Task<Result<IReadOnlyCollection<PermissionDetailsScheme>>> HandleAsync(
7+
AssignClientPermissionScheme parameters, CancellationToken cancellation = default)
8+
{
9+
var clientFilters = ClientFilters.WithSpecifications()
10+
.WithIdentifier(parameters.Id)
11+
.Build();
12+
13+
var permissionFilters = PermissionFilters.WithSpecifications()
14+
.WithName(parameters.PermissionName.ToLower())
15+
.Build();
16+
17+
var clients = await clientCollection.GetClientsAsync(clientFilters, cancellation: cancellation);
18+
var client = clients.FirstOrDefault();
19+
20+
if (client is null)
21+
{
22+
return Result<IReadOnlyCollection<PermissionDetailsScheme>>.Failure(ClientErrors.ClientDoesNotExist);
23+
}
24+
25+
var permissions = await permissionCollection.GetPermissionsAsync(permissionFilters, cancellation: cancellation);
26+
var existingPermission = permissions.FirstOrDefault();
27+
28+
if (existingPermission is null)
29+
{
30+
return Result<IReadOnlyCollection<PermissionDetailsScheme>>.Failure(PermissionErrors.PermissionDoesNotExist);
31+
}
32+
33+
if (client.Permissions.Any(permission => permission.Name == existingPermission.Name))
34+
{
35+
return Result<IReadOnlyCollection<PermissionDetailsScheme>>.Failure(ClientErrors.ClientAlreadyHasPermission);
36+
}
37+
38+
client.Permissions.Add(existingPermission);
39+
40+
await clientCollection.UpdateAsync(client, cancellation: cancellation);
41+
42+
return Result<IReadOnlyCollection<PermissionDetailsScheme>>.Success(PermissionMapper.AsResponse(client.Permissions));
43+
}
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class ClientCreationHandler(IClientCredentialsGenerator credentialsGenerator, IRealmProvider realmProvider, IClientCollection clientCollection) :
4+
IDispatchHandler<ClientCreationScheme, Result<ClientScheme>>
5+
{
6+
public async Task<Result<ClientScheme>> HandleAsync(ClientCreationScheme parameters, CancellationToken cancellation = default)
7+
{
8+
var filters = ClientFilters.WithSpecifications()
9+
.WithName(parameters.Name)
10+
.Build();
11+
12+
var clients = await clientCollection.GetClientsAsync(filters, cancellation: cancellation);
13+
var existingClient = clients.FirstOrDefault();
14+
15+
if (existingClient is not null)
16+
{
17+
return Result<ClientScheme>.Failure(ClientErrors.ClientAlreadyExists);
18+
}
19+
20+
var realm = realmProvider.GetCurrentRealm();
21+
var credentials = await credentialsGenerator.GenerateAsync(parameters.Name, cancellation);
22+
23+
var client = parameters.AsClient(credentials, realm);
24+
25+
await clientCollection.InsertAsync(client, cancellation: cancellation);
26+
27+
return Result<ClientScheme>.Success(client.AsResponse());
28+
}
29+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class ClientDeletionHandler(IClientCollection collection) : IDispatchHandler<ClientDeletionScheme, Result>
4+
{
5+
public async Task<Result> HandleAsync(ClientDeletionScheme parameters, CancellationToken cancellation = default)
6+
{
7+
var filters = ClientFilters.WithSpecifications()
8+
.WithIdentifier(parameters.Id)
9+
.Build();
10+
11+
var clients = await collection.GetClientsAsync(filters, cancellation: cancellation);
12+
var client = clients.FirstOrDefault();
13+
14+
if (client is null)
15+
{
16+
return Result.Failure(ClientErrors.ClientDoesNotExist);
17+
}
18+
19+
await collection.DeleteAsync(client, cancellation: cancellation);
20+
21+
return Result.Success();
22+
}
23+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class ClientUpdateHandler(IClientCollection collection) :
4+
IDispatchHandler<ClientUpdateScheme, Result<ClientScheme>>
5+
{
6+
public async Task<Result<ClientScheme>> HandleAsync(
7+
ClientUpdateScheme parameters, CancellationToken cancellation = default)
8+
{
9+
var filters = ClientFilters.WithSpecifications()
10+
.WithIdentifier(parameters.Id)
11+
.Build();
12+
13+
var clients = await collection.GetClientsAsync(filters, cancellation: cancellation);
14+
var client = clients.FirstOrDefault();
15+
16+
if (client is null)
17+
{
18+
return Result<ClientScheme>.Failure(ClientErrors.ClientDoesNotExist);
19+
}
20+
21+
var nameFilter = ClientFilters.WithSpecifications()
22+
.WithName(parameters.Name)
23+
.Build();
24+
25+
var clientsWithSameName = await collection.GetClientsAsync(nameFilter, cancellation: cancellation);
26+
var existingClient = clientsWithSameName.FirstOrDefault(existing => existing.Id != parameters.Id);
27+
28+
if (existingClient is not null)
29+
{
30+
return Result<ClientScheme>.Failure(ClientErrors.ClientAlreadyExists);
31+
}
32+
33+
client = ClientMapper.AsClient(parameters, client);
34+
35+
var updatedClient = await collection.UpdateAsync(client, cancellation: cancellation);
36+
37+
return Result<ClientScheme>.Success(ClientMapper.AsResponse(updatedClient));
38+
}
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class FetchClientsHandler(IClientCollection clientCollection) :
4+
IDispatchHandler<ClientsFetchParameters, Result<Pagination<ClientScheme>>>
5+
{
6+
public async Task<Result<Pagination<ClientScheme>>> HandleAsync(
7+
ClientsFetchParameters parameters, CancellationToken cancellation = default)
8+
{
9+
var filters = ClientFilters.WithSpecifications()
10+
.WithName(parameters.Name)
11+
.WithClientId(parameters.ClientId)
12+
.WithSort(parameters.Sort)
13+
.WithPagination(parameters.Pagination)
14+
.Build();
15+
16+
var clients = await clientCollection.GetClientsAsync(filters, cancellation);
17+
var totalClients = await clientCollection.CountClientsAsync(filters, cancellation);
18+
19+
var pagination = new Pagination<ClientScheme>
20+
{
21+
Items = [.. clients.Select(client => ClientMapper.AsResponse(client))],
22+
Total = (int)totalClients,
23+
PageNumber = parameters.Pagination?.PageNumber ?? 1,
24+
PageSize = parameters.Pagination?.PageSize ?? 20,
25+
};
26+
27+
return Result<Pagination<ClientScheme>>.Success(pagination);
28+
}
29+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class ListClientAssignedPermissionsHandler(IClientCollection collection) :
4+
IDispatchHandler<ListClientAssignedPermissionsParameters, Result<IReadOnlyCollection<PermissionDetailsScheme>>>
5+
{
6+
public async Task<Result<IReadOnlyCollection<PermissionDetailsScheme>>> HandleAsync(
7+
ListClientAssignedPermissionsParameters parameters, CancellationToken cancellation = default)
8+
{
9+
var filters = ClientFilters.WithSpecifications()
10+
.WithIdentifier(parameters.Id)
11+
.Build();
12+
13+
var clients = await collection.GetClientsAsync(filters, cancellation);
14+
var client = clients.FirstOrDefault();
15+
16+
return client is not null
17+
? Result<IReadOnlyCollection<PermissionDetailsScheme>>.Success(PermissionMapper.AsResponse(client.Permissions))
18+
: Result<IReadOnlyCollection<PermissionDetailsScheme>>.Failure(ClientErrors.ClientDoesNotExist);
19+
}
20+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace HttpsRichardy.Federation.Application.Handlers.Client;
2+
3+
public sealed class RevokeAudienceFromClientHandler(IClientCollection clientCollection) :
4+
IDispatchHandler<RevokeClientAudienceScheme, Result<IReadOnlyCollection<string>>>
5+
{
6+
public async Task<Result<IReadOnlyCollection<string>>> HandleAsync(
7+
RevokeClientAudienceScheme parameters, CancellationToken cancellation = default)
8+
{
9+
var filters = ClientFilters.WithSpecifications()
10+
.WithIdentifier(parameters.Id)
11+
.Build();
12+
13+
var clients = await clientCollection.GetClientsAsync(filters, cancellation);
14+
var client = clients.FirstOrDefault();
15+
16+
if (client is null)
17+
{
18+
return Result<IReadOnlyCollection<string>>.Failure(ClientErrors.ClientDoesNotExist);
19+
}
20+
21+
var audienceToRemove = client.Audiences.FirstOrDefault(current => current.Value == parameters.Audience);
22+
if (audienceToRemove is null)
23+
{
24+
return Result<IReadOnlyCollection<string>>.Failure(ClientErrors.AudienceNotAssigned);
25+
}
26+
27+
client.Audiences.Remove(audienceToRemove);
28+
29+
await clientCollection.UpdateAsync(client, cancellation);
30+
31+
return Result<IReadOnlyCollection<string>>.Success([.. client.Audiences.Select(current => current.Value)]);
32+
}
33+
}

0 commit comments

Comments
 (0)