diff --git a/src/Turnierplan.App.Test.Functional/Routes.cs b/src/Turnierplan.App.Test.Functional/Routes.cs new file mode 100644 index 00000000..9de37e9e --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/Routes.cs @@ -0,0 +1,21 @@ +using Turnierplan.Core.PublicId; + +namespace Turnierplan.App.Test.Functional; + +internal static class Routes +{ + public static class ApiKeys + { + public static string Delete(PublicId id) => $"/api/api-keys/{id}"; + } + + public static class Identity + { + public static string Login() => "/api/identity/login"; + } + + public static class Users + { + public static string Delete(Guid id) => $"/api/users/{id}"; + } +} diff --git a/src/Turnierplan.App.Test.Functional/Scenarios.cs b/src/Turnierplan.App.Test.Functional/Scenarios.cs new file mode 100644 index 00000000..a8c1ea09 --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/Scenarios.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Extensions; +using Turnierplan.Core.Organization; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.User; +using Xunit; + +namespace Turnierplan.App.Test.Functional; + +public sealed class Scenarios +{ + private readonly TestServer _testServer = new(); + + [Fact] + public async Task When_ApiKey_And_User_Are_Deleted_The_Role_Assignments_Are_Also_Removed() + { + var (apiKeyId, userId) = _testServer.ExecuteContextAction(db => + { + var user = new User(string.Empty); + db.Users.Add(user); + + var org = new Organization(string.Empty); + db.Organizations.Add(org); + + var key = new ApiKey(org, string.Empty, null, DateTime.UtcNow + 1.Days()); + db.ApiKeys.Add(key); + + var tournament = new Tournament(org, string.Empty, Visibility.Public); + db.Tournaments.Add(tournament); + + org.AddRoleAssignment(Role.Reader, user.AsPrincipal()); + tournament.AddRoleAssignment(Role.Contributor, user.AsPrincipal()); + tournament.AddRoleAssignment(Role.Reader, key.AsPrincipal()); + + db.SaveChanges(); + + return (key.PublicId, user.Id); + }); + + _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1); + _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(2); + + var resp = await _testServer.Client.DeleteAsync(Routes.ApiKeys.Delete(apiKeyId), TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + + _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1); + _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(1); + + resp = await _testServer.Client.DeleteAsync(Routes.Users.Delete(userId), TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + + _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(0); + _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(0); + } +} diff --git a/src/Turnierplan.App.Test.Functional/TestServer.cs b/src/Turnierplan.App.Test.Functional/TestServer.cs new file mode 100644 index 00000000..92cb0ca0 --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/TestServer.cs @@ -0,0 +1,72 @@ +using System.Net.Http.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Turnierplan.Core.User; +using Turnierplan.Dal; + +namespace Turnierplan.App.Test.Functional; + +internal sealed class TestServer +{ + private readonly WebApplicationFactory _application; + + public TestServer() + { + _application = new WebApplicationFactory().WithWebHostBuilder(builder => + { + builder.ConfigureAppConfiguration(config => + { + config.AddInMemoryCollection([ + new KeyValuePair("Database:InMemory", "true"), + new KeyValuePair("Identity:UseInsecureCookies", "true") + ]); + }); + }); + + const string username = "functional_test"; + const string password = "P@ssw0rd"; + + using (var scope = _application.Services.CreateScope()) + { + var ctx = scope.ServiceProvider.GetRequiredService(); + + var user = new User(username) + { + IsAdministrator = true + }; + + user.UpdatePassword(scope.ServiceProvider.GetRequiredService>().HashPassword(user, password)); + + ctx.Users.Add(user); + ctx.SaveChanges(); + } + + var loginRequest = new HttpRequestMessage(HttpMethod.Post, Routes.Identity.Login()) + { + Content = JsonContent.Create(new { UserName = username, Password = password}) + }; + + Client = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true }); + var loginResponseTask = Client.SendAsync(loginRequest); + loginResponseTask.Wait(); + var loginResponse = loginResponseTask.Result; + loginResponse.EnsureSuccessStatusCode(); + } + + public HttpClient Client { get; } + + public void ExecuteContextAction(Action action) + { + using var scope = _application.Services.CreateScope(); + action(scope.ServiceProvider.GetRequiredService()); + } + + public T ExecuteContextAction(Func action) + { + using var scope = _application.Services.CreateScope(); + return action(scope.ServiceProvider.GetRequiredService()); + } +} diff --git a/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj b/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj new file mode 100644 index 00000000..fe0ea8e1 --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj @@ -0,0 +1,19 @@ + + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts index b4fbdd34..06af01e2 100644 --- a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts +++ b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts @@ -26,15 +26,32 @@ export const turnierplan = { navigationTab: (id: number) => `page-frame-navigation-tab-${id}`, title: 'page-frame-title' }, + rbacWidget: { + openOffcanvasButton: 'rbac-widget-open-offcanvas-button' + }, + rbacOffcanvas: { + doneButton: 'rbac-offcanvas-done-button', + assignmentsCount: 'rbac-offcanvas-assignments-count' + }, viewOrganizationPage: { tournamentsPageId: 0, + apiKeysPageId: 2, settingsPageId: 3, - newTournamentButton: 'view-organization-page-new-tournament-button' + newTournamentButton: 'view-organization-page-new-tournament-button', + newApiKeyButton: 'view-organization-page-new-api-key-button', + deleteApiKeyButton: (id: string) => `view-organization-page-delete-api-key-button-${id}` }, createTournamentPage: { confirmButton: 'create-tournament-page-confirm-button', tournamentNameField: 'create-tournament-page-tournament-name-field' }, + createApiKeyPage: { + apiKeyNameField: 'create-api-key-page-api-key-name-field', + confirmButton: 'create-api-key-page-confirm-button', + resultIdField: 'create-api-key-page-result-id-field', + resultSecretField: 'create-api-key-page-result-secret-field', + doneButton: 'create-api-key-page-done-button' + }, configureTournamentPage: { addGroupButton: 'configure-tournament-page-add-group-button', shuffleGroupsButton: 'configure-tournament-page-shuffle-groups-button', diff --git a/src/Turnierplan.App/Client/e2e/pages/rbac-offcanvas.ts b/src/Turnierplan.App/Client/e2e/pages/rbac-offcanvas.ts new file mode 100644 index 00000000..4cc997b0 --- /dev/null +++ b/src/Turnierplan.App/Client/e2e/pages/rbac-offcanvas.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; +import { turnierplan } from '../consts/turnierplan'; + +export class RbacOffcanvas { + constructor(private readonly page: Page) {} + + public getRoleAssignmentsCountLocator(): Locator { + return this.page.getByTestId(turnierplan.rbacOffcanvas.assignmentsCount); + } + + public async close(): Promise { + await this.page.getByTestId(turnierplan.rbacOffcanvas.doneButton).click(); + } +} diff --git a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts index 73ba5271..552f2347 100644 --- a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts +++ b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts @@ -11,10 +11,38 @@ export class ViewOrganizationPage { await this.page.getByTestId(turnierplan.createTournamentPage.confirmButton).click(); } - public async deleteOrganization(confirmText: string): Promise { + public async createApiKey(name: string): Promise<{ id: string; secret: string }> { + await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.apiKeysPageId)).click(); + await this.page.getByTestId(turnierplan.viewOrganizationPage.newApiKeyButton).click(); + await this.page.getByTestId(turnierplan.createApiKeyPage.apiKeyNameField).fill(name); + await this.page.getByTestId(turnierplan.createApiKeyPage.confirmButton).click(); + + await this.page.getByTestId(turnierplan.createApiKeyPage.resultIdField).waitFor({ state: 'visible' }); + const resultId = await this.page.getByTestId(turnierplan.createApiKeyPage.resultIdField).inputValue(); + const resultSecret = await this.page.getByTestId(turnierplan.createApiKeyPage.resultSecretField).inputValue(); + + await this.page.getByTestId(turnierplan.createApiKeyPage.doneButton).click(); + + return { id: resultId, secret: resultSecret }; + } + + public async deleteApiKeyWithId(id: string, name: string): Promise { + await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.apiKeysPageId)).click(); + await this.page.getByTestId(turnierplan.viewOrganizationPage.deleteApiKeyButton(id)).click(); + await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(name); + await this.page.getByTestId(turnierplan.deleteWidget.deleteButton).click(); + await this.page.getByTestId(turnierplan.deleteModal.confirmDeleteButton).click(); + } + + public async deleteOrganization(name: string): Promise { await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.settingsPageId)).click(); - await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(confirmText); + await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(name); await this.page.getByTestId(turnierplan.deleteWidget.deleteButton).click(); await this.page.getByTestId(turnierplan.deleteModal.confirmDeleteButton).click(); } + + public async openRoleAssignments(): Promise { + await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.settingsPageId)).click(); + await this.page.getByTestId(turnierplan.rbacWidget.openOffcanvasButton).click(); + } } diff --git a/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts b/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts new file mode 100644 index 00000000..ae4e099a --- /dev/null +++ b/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test'; +import { createIdentifier } from '../utils/create-identifier'; +import { LoginPage } from '../pages/login-page'; +import { LandingPage } from '../pages/landing-page'; +import { ViewOrganizationPage } from '../pages/view-organization-page'; +import { RbacOffcanvas } from '../pages/rbac-offcanvas'; + +test('Create API key, then create and remove role assignment', async ({ page }) => { + const organizationName = createIdentifier(); + + await new LoginPage(page).login(); + await new LandingPage(page).createOrganization(organizationName); + + const organizationPage = new ViewOrganizationPage(page); + const rbacOffcanvas = new RbacOffcanvas(page); + + await organizationPage.openRoleAssignments(); + await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('1 Zuweisung'); + await rbacOffcanvas.close(); + + const apiKeyName = createIdentifier(); + const apiKeyDetails = await organizationPage.createApiKey(apiKeyName); + + await organizationPage.openRoleAssignments(); + await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('2 Zuweisungen'); + await rbacOffcanvas.close(); + + await organizationPage.deleteApiKeyWithId(apiKeyDetails.id, apiKeyName); + + await organizationPage.openRoleAssignments(); + await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('1 Zuweisung'); + await rbacOffcanvas.close(); +}); diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index cc9d430c..5ffc1f30 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -1393,7 +1393,10 @@ export const de = { User: 'Benutzer' }, PrincipalNotFound: 'Der Prinzipal konnte nicht mehr gefunden werden. Wurde der Nutzer/API-Schlüssel gelöscht?', - TotalCount: '{{count}} Zuweisungen', + TotalCount: { + One: '1 Zuweisung', + Many: '{{count}} Zuweisungen' + }, Id: 'ID:', CreatedAt: 'Erstellt am:', Inherited: 'Vererbt durch:', diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html index 35886acc..935f066c 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html @@ -1,6 +1,7 @@
- @if (roleAssignmentCount > 3) { -
- -
- } +
+ @if (roleAssignmentCount === 1) { + + } @else { + + } +
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts index 98cf18cd..d4f96e7f 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts @@ -14,6 +14,7 @@ import { RoleAssignmentDto } from '../../../api/models/role-assignment-dto'; import { TurnierplanApi } from '../../../api/turnierplan-api'; import { deleteRoleAssignment } from '../../../api/fn/role-assignments/delete-role-assignment'; import { getRoleAssignments } from '../../../api/fn/role-assignments/get-role-assignments'; +import { E2eDirective } from '../../../core/directives/e2e.directive'; interface IRbacOffcanvasTarget { name: string; @@ -30,7 +31,8 @@ interface IRbacOffcanvasTarget { RbacPrincipalComponent, DeleteButtonComponent, TranslatePipe, - TranslateDatePipe + TranslateDatePipe, + E2eDirective ] }) export class RbacOffcanvasComponent implements OnDestroy { diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index 923187b0..ff6003b5 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -10,6 +10,7 @@
- + @@ -15,7 +21,13 @@
- + @@ -25,7 +37,11 @@
- +
} @else { @@ -38,6 +54,7 @@ type="text" class="form-control" formControlName="name" + [tpE2E]="'create-api-key-page-api-key-name-field'" [ngClass]="nameControl.dirty || nameControl.touched ? (nameControl.invalid ? 'is-invalid' : 'is-valid') : ''" />
@@ -90,6 +107,7 @@
Handle( [FromRoute] PublicId id, IApiKeyRepository repository, IAccessValidator accessValidator, + IServiceProvider serviceProvider, CancellationToken cancellationToken) { var apiKey = await repository.GetByPublicIdAsync(id); @@ -33,6 +35,13 @@ private static async Task Handle( repository.Remove(apiKey); + var principal = apiKey.AsPrincipal(); + + foreach (var roleAssignmentRepository in serviceProvider.GetServices()) + { + await roleAssignmentRepository.RemoveAllByPrincipalAsync(principal); + } + await repository.UnitOfWork.SaveChangesAsync(cancellationToken); return Results.NoContent(); diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs index a3eaa6c5..39f134bf 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs @@ -30,16 +30,8 @@ internal sealed class CreateRoleAssignmentEndpoint : EndpointBase Handle( [FromBody] CreateRoleAssignmentEndpointRequest request, - IApiKeyRepository apiKeyRepository, - IFolderRepository folderRepository, - IImageRepository imageRepository, - IOrganizationRepository organizationRepository, - IPlanningRealmRepository planningRealmRepository, - ITournamentRepository tournamentRepository, - IUserRepository userRepository, - IVenueRepository venueRepository, - IAccessValidator accessValidator, IServiceProvider serviceProvider, + IAccessValidator accessValidator, IMapper mapper, CancellationToken cancellationToken) { @@ -55,13 +47,13 @@ private static async Task Handle( var task = typeName switch { - "ApiKey" => CreateRoleAssignmentAsync(request, apiKeyRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Folder" => CreateRoleAssignmentAsync(request, folderRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Image" => CreateRoleAssignmentAsync(request, imageRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Organization" => CreateRoleAssignmentAsync(request, organizationRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "PlanningRealm" => CreateRoleAssignmentAsync(request, planningRealmRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Tournament" => CreateRoleAssignmentAsync(request, tournamentRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Venue" => CreateRoleAssignmentAsync(request, venueRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + "ApiKey" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Folder" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Image" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Organization" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "PlanningRealm" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Tournament" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Venue" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), _ => null }; @@ -72,16 +64,14 @@ private static async Task Handle( private static async Task CreateRoleAssignmentAsync( CreateRoleAssignmentEndpointRequest request, - IRepositoryWithPublicId repository, - PublicId targetId, + IServiceProvider serviceProvider, IAccessValidator accessValidator, - IApiKeyRepository apiKeyRepository, - IUserRepository userRepository, - IRoleAssignmentRepository roleAssignmentRepository, IMapper mapper, + PublicId targetId, CancellationToken cancellationToken) where T : Entity, IEntityWithRoleAssignments { + var repository = serviceProvider.GetRequiredService>(); var entity = await repository.GetByPublicIdAsync(targetId); if (entity is null) @@ -94,7 +84,7 @@ private static async Task CreateRoleAssignmentAsync( return Results.Forbid(); } - var principal = await GetPrincipalAsync(request, apiKeyRepository, userRepository); + var principal = await GetPrincipalAsync(request, serviceProvider); if (principal is null) { @@ -106,6 +96,7 @@ private static async Task CreateRoleAssignmentAsync( return Results.Conflict("There already exists a role assignment for the specified principal/role combination."); } + var roleAssignmentRepository = serviceProvider.GetRequiredService>(); var roleAssignment = entity.AddRoleAssignment(request.Role, principal); await roleAssignmentRepository.CreateAsync(roleAssignment); @@ -114,13 +105,11 @@ private static async Task CreateRoleAssignmentAsync( return Results.Ok(mapper.Map(roleAssignment)); } - private static async Task GetPrincipalAsync( - CreateRoleAssignmentEndpointRequest request, - IApiKeyRepository apiKeyRepository, - IUserRepository userRepository) + private static async Task GetPrincipalAsync(CreateRoleAssignmentEndpointRequest request, IServiceProvider serviceProvider) { if (request.ApiKeyId.HasValue) { + var apiKeyRepository = serviceProvider.GetRequiredService(); var apiKey = await apiKeyRepository.GetByPublicIdAsync(request.ApiKeyId.Value); return apiKey?.AsPrincipal(); @@ -128,6 +117,7 @@ private static async Task CreateRoleAssignmentAsync( if (request.UserNameOrEmail is not null) { + var userRepository = serviceProvider.GetRequiredService(); var user = await userRepository.GetByUserNameOrEmailAsync(request.UserNameOrEmail); return user?.AsPrincipal(); diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs index bd739911..795d7efb 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs @@ -1,10 +1,16 @@ using Microsoft.AspNetCore.Mvc; using Turnierplan.App.Helpers; using Turnierplan.App.Security; +using Turnierplan.Core.ApiKey; using Turnierplan.Core.Entity; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; using Turnierplan.Core.Organization; +using Turnierplan.Core.PlanningRealm; using Turnierplan.Core.PublicId; using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; using Turnierplan.Dal.Repositories; namespace Turnierplan.App.Endpoints.RoleAssignments; @@ -20,13 +26,7 @@ internal sealed class DeleteRoleAssignmentEndpoint : EndpointBase private static async Task Handle( [FromRoute] string scopeId, [FromRoute] string roleAssignmentId, - IApiKeyRepository apiKeyRepository, - IFolderRepository folderRepository, - IImageRepository imageRepository, - IOrganizationRepository organizationRepository, - IPlanningRealmRepository planningRealmRepository, - ITournamentRepository tournamentRepository, - IVenueRepository venueRepository, + IServiceProvider serviceProvider, IAccessValidator accessValidator, CancellationToken cancellationToken) { @@ -42,13 +42,13 @@ private static async Task Handle( var task = typeName switch { - "ApiKey" => DeleteRoleAssignmentAsync(apiKeyRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Folder" => DeleteRoleAssignmentAsync(folderRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Image" => DeleteRoleAssignmentAsync(imageRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Organization" => DeleteRoleAssignmentAsync(organizationRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "PlanningRealm" => DeleteRoleAssignmentAsync(planningRealmRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Tournament" => DeleteRoleAssignmentAsync(tournamentRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Venue" => DeleteRoleAssignmentAsync(venueRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + "ApiKey" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Folder" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Image" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Organization" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "PlanningRealm" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Tournament" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Venue" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), _ => null }; @@ -58,13 +58,14 @@ private static async Task Handle( } private static async Task DeleteRoleAssignmentAsync( - IRepositoryWithPublicId repository, - PublicId targetId, + IServiceProvider serviceProvider, IAccessValidator accessValidator, + PublicId targetId, Guid roleAssignmentId, CancellationToken cancellationToken) where T : Entity, IEntityWithRoleAssignments { + var repository = serviceProvider.GetRequiredService>(); var entity = await repository.GetByPublicIdAsync(targetId); if (entity is null) diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs index c05f5c39..2377d317 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs @@ -3,9 +3,15 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; +using Turnierplan.Core.ApiKey; using Turnierplan.Core.Entity; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; +using Turnierplan.Core.Organization; +using Turnierplan.Core.PlanningRealm; using Turnierplan.Core.PublicId; using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; using Turnierplan.Dal.Repositories; namespace Turnierplan.App.Endpoints.RoleAssignments; @@ -20,13 +26,7 @@ internal sealed class GetRoleAssignmentsEndpoint : EndpointBase Handle( [FromRoute] string scopeId, - IApiKeyRepository apiKeyRepository, - IFolderRepository folderRepository, - IImageRepository imageRepository, - IOrganizationRepository organizationRepository, - IPlanningRealmRepository planningRealmRepository, - ITournamentRepository tournamentRepository, - IVenueRepository venueRepository, + IServiceProvider serviceProvider, IAccessValidator accessValidator, IMapper mapper) { @@ -37,13 +37,13 @@ private static async Task Handle( var task = typeName switch { - "ApiKey" => GetRoleAssignmentsAsync(apiKeyRepository, targetId, accessValidator, mapper), - "Folder" => GetRoleAssignmentsAsync(folderRepository, targetId, accessValidator, mapper), - "Image" => GetRoleAssignmentsAsync(imageRepository, targetId, accessValidator, mapper), - "Organization" => GetRoleAssignmentsAsync(organizationRepository, targetId, accessValidator, mapper), - "PlanningRealm" => GetRoleAssignmentsAsync(planningRealmRepository, targetId, accessValidator, mapper), - "Tournament" => GetRoleAssignmentsAsync(tournamentRepository, targetId, accessValidator, mapper), - "Venue" => GetRoleAssignmentsAsync(venueRepository, targetId, accessValidator, mapper), + "ApiKey" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Folder" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Image" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Organization" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "PlanningRealm" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Tournament" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Venue" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), _ => null }; @@ -52,9 +52,10 @@ private static async Task Handle( : await task; } - private static async Task GetRoleAssignmentsAsync(IRepositoryWithPublicId repository, PublicId targetId, IAccessValidator accessValidator, IMapper mapper) + private static async Task GetRoleAssignmentsAsync(IServiceProvider serviceProvider, IAccessValidator accessValidator, IMapper mapper, PublicId targetId) where T : Entity, IEntityWithRoleAssignments { + var repository = serviceProvider.GetRequiredService>(); var entity = await repository.GetByPublicIdAsync(targetId); if (entity is null) diff --git a/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs b/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs index f1a089e3..87ffeff4 100644 --- a/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Turnierplan.App.Extensions; +using Turnierplan.Core.Extensions; using Turnierplan.Dal.Repositories; namespace Turnierplan.App.Endpoints.Users; @@ -18,6 +19,7 @@ private static async Task Handle( [FromRoute] Guid id, HttpContext context, IUserRepository repository, + IServiceProvider serviceProvider, CancellationToken cancellationToken) { if (id == context.GetCurrentUserIdOrThrow()) @@ -34,6 +36,13 @@ private static async Task Handle( repository.Remove(user); + var principal = user.AsPrincipal(); + + foreach (var roleAssignmentRepository in serviceProvider.GetServices()) + { + await roleAssignmentRepository.RemoveAllByPrincipalAsync(principal); + } + await repository.UnitOfWork.SaveChangesAsync(cancellationToken); return Results.NoContent(); diff --git a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs index 0660ece7..4c05efa8 100644 --- a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Document; using Turnierplan.Core.Folder; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; @@ -59,6 +60,16 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped(); services.AddScoped(); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped, ApiKeyRoleAssignmentRepository>(); services.AddScoped, FolderRoleAssignmentRepository>(); services.AddScoped, ImageRoleAssignmentRepository>(); @@ -66,5 +77,13 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped, PlanningRealmRoleAssignmentRepository>(); services.AddScoped, TournamentRoleAssignmentRepository>(); services.AddScoped, VenueRoleAssignmentRepository>(); + + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); } } diff --git a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs index 4eb3dc0a..3b959770 100644 --- a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs +++ b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using Turnierplan.Core.ApiKey; using Turnierplan.Core.Entity; using Turnierplan.Core.Folder; @@ -10,11 +11,23 @@ namespace Turnierplan.Dal.Repositories; -public interface IRoleAssignmentRepository : IRepository, Guid> +public interface IRoleAssignmentRepository +{ + Task RemoveAllByPrincipalAsync(Principal principal); +} + +public interface IRoleAssignmentRepository : IRepository, Guid>, IRoleAssignmentRepository where T : Entity, IEntityWithRoleAssignments; internal abstract class RoleAssignmentRepositoryBase(TurnierplanContext context) : RepositoryBase, Guid>(context), IRoleAssignmentRepository - where T : Entity, IEntityWithRoleAssignments; + where T : Entity, IEntityWithRoleAssignments +{ + public async Task RemoveAllByPrincipalAsync(Principal principal) + { + var roleAssignments = await DbSet.Where(x => x.Principal == principal).ToListAsync(); + DbSet.RemoveRange(roleAssignments); + } +} internal sealed class ApiKeyRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context); diff --git a/src/turnierplan.NET.slnx b/src/turnierplan.NET.slnx index 03a88477..b163cf28 100644 --- a/src/turnierplan.NET.slnx +++ b/src/turnierplan.NET.slnx @@ -5,6 +5,7 @@ +