{"record":{"id":"342da2d89bfe025a","repo":"fullstackhero/dotnet-starter-kit","slug":"no-current-user-deletemessagecommandhandler","errorCode":null,"errorMessage":"no current user","messagePattern":"no current user","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Messages/DeleteMessage/DeleteMessageCommandHandler.cs","lineNumber":26,"sourceCode":"using FSH.Modules.Identity.Contracts.Services;\nusing Mediator;\nusing Microsoft.AspNetCore.SignalR;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Chat.Features.v1.Messages.DeleteMessage;\n\npublic sealed class DeleteMessageCommandHandler(\n    ChatDbContext db,\n    ICurrentUser currentUser,\n    IUserPermissionService permissions,\n    IHubContext<AppHub> hub)\n    : ICommandHandler<DeleteMessageCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(DeleteMessageCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        var userId = currentUser.GetUserId();\n        if (userId == Guid.Empty) throw new UnauthorizedException(\"no current user\");\n        var currentUserId = userId.ToString();\n\n        var message = await db.Messages.FirstOrDefaultAsync(m => m.Id == cmd.MessageId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Message not found.\");\n\n        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == message.ChannelId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Message not found.\");\n        channel.RequireMember(currentUserId);\n\n        bool isModerator = await permissions\n            .HasPermissionAsync(currentUserId, ChatPermissions.Messages.DeleteAny, cancellationToken)\n            .ConfigureAwait(false);\n\n        message.SoftDelete(currentUserId, isModerator);\n\n        // If this was a thread reply, decrement the parent's ReplyCount.","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Messages/DeleteMessage/DeleteMessageCommandHandler.cs#L8-L44","documentation":"DeleteMessageCommandHandler throws UnauthorizedException(\"no current user\") when currentUser.GetUserId() returns Guid.Empty, i.e. the request carries no authenticated user identity. The guard runs before any database access so deletes are never attempted anonymously. Rendered as HTTP 401 by the module's exception handling.","triggerScenarios":"Calling the delete-message endpoint without a valid JWT; invoking the handler from a Hangfire job, SignalR invocation, or test where no ICurrentUser is established.","commonSituations":"Expired access token not refreshed by the frontend; missing [Authorize] on the endpoint letting anonymous calls through to the handler; SignalR hub methods invoked before negotiation completes authentication; test doubles returning default(Guid).","solutions":["Send a valid, unexpired bearer token with the request.","Ensure the endpoint/hub method requires authentication and auth middleware runs before handlers.","Implement token refresh on 401 in the frontend apiFetch layer.","In tests/background jobs, provide a mocked ICurrentUser returning a concrete Guid."],"exampleFix":"// before\nvar response = await fetch(`/api/v1/messages/${id}`, { method: \"DELETE\" }); // 401\n\n// after\napiFetch(`/api/v1/messages/${id}`, { method: \"DELETE\" }); // attaches bearer token + refreshes","handlingStrategy":"try-catch","validationCode":"if (!auth.accessToken || isExpired(auth.accessToken)) {\n  await refreshAccessToken(); // authenticate before deleting\n}","typeGuard":"function hasIdentity(u: { id?: string }): u is { id: string } {\n  return typeof u.id === \"string\" && u.id !== \"\" && u.id !== \"00000000-0000-0000-0000-000000000000\";\n}","tryCatchPattern":"try {\n  await api.deleteMessage(messageId);\n} catch (e) {\n  if (e.status === 401) { await refreshAndRetry(); }\n  else throw e;\n}","preventionTips":["Route all API calls through an authenticated fetch wrapper.","Handle 401 globally with a token refresh + retry.","Disable destructive actions until auth state is resolved.","Keep [Authorize] on delete endpoints."],"tags":["auth","unauthorized","chat","current-user"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}