Handler.cs 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Helpers;
  4. using Domain.Entities.Developers;
  5. using Microsoft.EntityFrameworkCore;
  6. using SharedKernel.Results;
  7. namespace Application.Features.Api.Developers.Tokens.IssuePat;
  8. internal sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result<Response>>
  9. {
  10. private const int MaxActivePatPerMember = 10;
  11. public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
  12. {
  13. if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100)
  14. {
  15. return Result.Failure<Response>(Error.Problem("Pat.NameInvalid", "Name is required (1-100 chars)."));
  16. }
  17. var activeCount = await db.ApiPersonalAccessToken
  18. .CountAsync(c => c.OwnerMemberID == request.OwnerMemberID && c.RevokedAt == null, ct);
  19. if (activeCount >= MaxActivePatPerMember)
  20. {
  21. return Result.Failure<Response>(Error.Problem("Pat.LimitExceeded", $"Max {MaxActivePatPerMember} active PAT per member."));
  22. }
  23. var token = ApiTokenGenerator.GeneratePat();
  24. var scopesCsv = string.Join(",", (request.Scopes ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()));
  25. var pat = ApiPersonalAccessToken.Create(
  26. request.OwnerMemberID,
  27. request.Name,
  28. ApiTokenGenerator.Hash(token),
  29. ApiTokenGenerator.Hint(token),
  30. scopesCsv,
  31. request.ExpiresAt
  32. );
  33. await db.ApiPersonalAccessToken.AddAsync(pat, ct);
  34. await db.SaveChangesAsync(ct);
  35. return Result.Success(new Response(
  36. pat.ID,
  37. pat.Name,
  38. token,
  39. pat.TokenHint,
  40. scopesCsv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
  41. pat.ExpiresAt,
  42. pat.CreatedAt
  43. ));
  44. }
  45. }