{"record":{"id":"6b1a5b5354d54393","repo":"fullstackhero/dotnet-starter-kit","slug":"cannot-create-a-ticket-without-an-authenticated-reporter","errorCode":null,"errorMessage":"Cannot create a ticket without an authenticated reporter.","messagePattern":"Cannot create a ticket without an authenticated reporter\\.","errorType":"exception","errorClass":"CustomException","httpStatus":401,"severity":"error","filePath":"src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/CreateTicket/CreateTicketCommandHandler.cs","lineNumber":26,"sourceCode":"using Mediator;\nusing FSH.Framework.Persistence;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Tickets.Features.v1.Tickets.CreateTicket;\n\npublic sealed class CreateTicketCommandHandler(\n    TicketsDbContext dbContext,\n    ICurrentUser currentUser)\n    : ICommandHandler<CreateTicketCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(CreateTicketCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var reporterId = currentUser.GetUserId();\n        if (reporterId == Guid.Empty)\n        {\n            throw new CustomException(\n                \"Cannot create a ticket without an authenticated reporter.\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.Unauthorized);\n        }\n\n        // Sequential, tenant-scoped ticket numbers (TK-1, …). Count ALL rows incl. soft-deleted so a\n        // deleted number isn't reused; racing writers collide on the unique index (→ 409, retryable).\n        long count = await dbContext.Tickets\n            .IgnoreQueryFilters([QueryFilters.SoftDelete])\n            .LongCountAsync(cancellationToken)\n            .ConfigureAwait(false);\n        string number = $\"TK-{(count + 1).ToString(CultureInfo.InvariantCulture)}\";\n\n        var ticket = Ticket.Create(\n            number: number,\n            title: command.Title,\n            description: command.Description,\n            priority: command.Priority,","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/CreateTicket/CreateTicketCommandHandler.cs#L8-L44","documentation":"CreateTicketCommandHandler resolves the reporter via currentUser.GetUserId(); a Guid.Empty result (unauthenticated or anonymous principal) triggers a 401 CustomException because every ticket must record who reported it. The ticket number sequence (TK-n) also depends on a valid tenant/authenticated context.","triggerScenarios":"Posting CreateTicketCommand without a valid JWT, with an expired token, or via an endpoint missing its authorization requirement so ICurrentUser has no user id.","commonSituations":"Scheduled jobs or webhooks calling the create endpoint with service credentials not configured; frontend losing the token after refresh; tests constructing the handler with a default/mock ICurrentUser returning Guid.Empty.","solutions":["Attach a valid Bearer token (or service-identity token) to the create-ticket request.","Refresh expired credentials and retry.","For machine callers, provision a dedicated service account and authenticate as it before creating tickets.","In tests, stub ICurrentUser.GetUserId() to return a real user Guid."],"exampleFix":"// before\nvar handler = new CreateTicketCommandHandler(dbContext, currentUserSubstitute); // GetUserId() => Guid.Empty\n// after\nvar currentUser = Substitute.For<ICurrentUser>();\ncurrentUser.GetUserId().Returns(Guid.NewGuid());\nvar handler = new CreateTicketCommandHandler(dbContext, currentUser);","handlingStrategy":"validation","validationCode":"if (string.IsNullOrWhiteSpace(token) || jwtSecurityToken == null)\n    throw new InvalidOperationException(\"A valid token is required to create a ticket.\");","typeGuard":"static bool CanCreateTicket(ICurrentUser user) => user.GetUserId() != Guid.Empty;","tryCatchPattern":"try { await mediator.Send(new CreateTicketCommand { ... }); }\ncatch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) { await signInAgain(); }","preventionTips":["Gate the create-ticket route behind authentication so the token always exists.","Provision service accounts for machine callers that create tickets.","Refresh tokens proactively before long-running UI sessions submit forms.","In handler tests, always stub GetUserId with a non-empty Guid."],"tags":["auth","tickets","creation","unauthorized"],"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"}