| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Command.Input; |
| | | 3 | | using ArturRios.Heimdall.Command.Output; |
| | | 4 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 5 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 6 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 7 | | using ArturRios.Heimdall.Shared.Services; |
| | | 8 | | using ArturRios.Mediator.Command.Interfaces; |
| | | 9 | | using ArturRios.Output; |
| | | 10 | | using Microsoft.EntityFrameworkCore; |
| | | 11 | | |
| | | 12 | | namespace ArturRios.Heimdall.Command.Handlers; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Handles <see cref="DeleteGoogleUserCommand" /> (UC-28, FR-GO-15): locates the Google User |
| | | 16 | | /// inside the addressed scope (AF-28a), enforces the scope-ownership rule (AF-28c), serves an |
| | | 17 | | /// already-deleted record as an idempotent no-op (AF-28b), and otherwise sets |
| | | 18 | | /// <c>IsDeleted = true</c> and stamps <c>UpdatedAt</c>. Nothing cascades — see the remarks. All |
| | | 19 | | /// failures are returned as errors on the <see cref="DataOutput{T}" /> rather than thrown. |
| | | 20 | | /// </summary> |
| | | 21 | | /// <remarks> |
| | | 22 | | /// <para> |
| | | 23 | | /// The shape of <see cref="DeleteApplicationCommandHandler" />, differing in step 2 only. |
| | | 24 | | /// UC-19 compares the application's <em>owner</em>, because owning a scope is not grounds to |
| | | 25 | | /// delete another owner's application; a Google User has no owner, only a scope (FR-GO-06), |
| | | 26 | | /// and UC-28 names "a System Admin, or an owner of the Google User's scope" — which is |
| | | 27 | | /// exactly what <see cref="IScopeOwnershipChecker" /> decides. |
| | | 28 | | /// </para> |
| | | 29 | | /// <para> |
| | | 30 | | /// Nothing cascades because there is nothing to cascade to. A Google User can own no |
| | | 31 | | /// application — FR-AP-03 restricts that to a <c>ScopeAdmin</c> who owns the scope, and a |
| | | 32 | | /// Google User is always <c>User</c>-equivalent (FR-GO-04) — and it holds no password reset |
| | | 33 | | /// or email verification tokens, since authentication is delegated to Google. |
| | | 34 | | /// </para> |
| | | 35 | | /// <para> |
| | | 36 | | /// The flag this sets is already honoured everywhere it matters: UC-25 refuses to |
| | | 37 | | /// authenticate a deleted Google User (AF-25d), UC-26 refuses their sign-out (AF-26a), and |
| | | 38 | | /// UC-27 hides them from default reads (FR-GO-17). |
| | | 39 | | /// </para> |
| | | 40 | | /// </remarks> |
| | 15 | 41 | | public class DeleteGoogleUserCommandHandler( |
| | 15 | 42 | | IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader, |
| | 15 | 43 | | IAsyncRepository<GoogleUser, long> googleUserWriter, |
| | 15 | 44 | | IScopeOwnershipChecker scopeOwnership) |
| | | 45 | | : ICommandHandlerAsync<DeleteGoogleUserCommand, DeleteGoogleUserCommandOutput> |
| | | 46 | | { |
| | | 47 | | public async Task<DataOutput<DeleteGoogleUserCommandOutput?>> HandleAsync(DeleteGoogleUserCommand command, Cancellat |
| | | 48 | | { |
| | 15 | 49 | | var output = DataOutput<DeleteGoogleUserCommandOutput?>.New; |
| | | 50 | | |
| | | 51 | | // AF-28a: the Google User must exist inside the addressed scope. The lookup deliberately omits |
| | | 52 | | // the !IsDeleted filter UC-27's default read applies, so an already-deleted record is found |
| | | 53 | | // and served idempotently by AF-28b below rather than reported as not found — without that, |
| | | 54 | | // AF-28b could never fire. The route's scopeId qualifies it: a Google User that belongs to |
| | | 55 | | // another scope is not the resource this path addresses. |
| | 15 | 56 | | var googleUser = await googleUserReader.Query() |
| | 15 | 57 | | .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId); |
| | | 58 | | |
| | 15 | 59 | | if (googleUser is null) |
| | | 60 | | { |
| | 4 | 61 | | return output.WithError(GoogleUserMessages.GoogleUserNotFound); |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | // AF-28c (UC-28 step 2): a System Admin may delete any Google User; a Scope Admin only those |
| | | 65 | | // of the scopes they own — which is what the checker decides, along with its guard that a |
| | | 66 | | // logically deleted actor owns nothing. Runs before AF-28b so an already-deleted Google User |
| | | 67 | | // cannot be used to probe for records outside the caller's reach. |
| | 11 | 68 | | if (!await scopeOwnership.ActorMayManageScopeAsync( |
| | 11 | 69 | | command.ActingRole, command.ActingPersonId, googleUser.ScopeId)) |
| | | 70 | | { |
| | 3 | 71 | | return output.WithError(GoogleUserMessages.NotAuthorizedToDeleteGoogleUser); |
| | | 72 | | } |
| | | 73 | | |
| | | 74 | | // AF-28b: already deleted — whether directly or by UC-04's cascade from its scope — so there |
| | | 75 | | // is nothing to write. UpdatedAt is left alone: the row already carries the requested state, |
| | | 76 | | // and re-stamping it would misreport when the deletion happened. |
| | 8 | 77 | | if (googleUser.IsDeleted) |
| | | 78 | | { |
| | 2 | 79 | | return output |
| | 2 | 80 | | .WithData(new DeleteGoogleUserCommandOutput { Id = googleUser.PublicId, AlreadyDeleted = true }) |
| | 2 | 81 | | .WithMessage(GoogleUserMessages.GoogleUserDeletedSuccessfully); |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | // UC-28 step 3: flip the flag and stamp UpdatedAt (no DB trigger maintains it). |
| | | 85 | | // DeletedAt and DeletionKind are stamped for NFR-20, for the reasons given in |
| | | 86 | | // DeletePersonCommandHandler: the anonymisation window is measured from the deletion, and |
| | | 87 | | // UpdatedAt would restart it on any later write. |
| | 6 | 88 | | var deletedAt = DateTime.UtcNow; |
| | | 89 | | |
| | 6 | 90 | | googleUser.IsDeleted = true; |
| | 6 | 91 | | googleUser.DeletedAt = deletedAt; |
| | 6 | 92 | | googleUser.DeletionKind = (int)DeletionKinds.Administrative; |
| | 6 | 93 | | googleUser.UpdatedAt = deletedAt; |
| | | 94 | | |
| | 6 | 95 | | var update = await googleUserWriter.UpdateAsync(googleUser); |
| | | 96 | | |
| | 6 | 97 | | if (!update.Success) |
| | | 98 | | { |
| | 0 | 99 | | return output.WithErrors(update.Errors); |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | // UC-28 step 4. |
| | 6 | 103 | | return output |
| | 6 | 104 | | .WithData(new DeleteGoogleUserCommandOutput { Id = googleUser.PublicId, AlreadyDeleted = false }) |
| | 6 | 105 | | .WithMessage(GoogleUserMessages.GoogleUserDeletedSuccessfully); |
| | 15 | 106 | | } |
| | | 107 | | } |