using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Helpers; using Domain.Entities.Developers; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Developers.Tokens.IssuePat; internal sealed class Handler(IAppDbContext db) : ICommandHandler> { private const int MaxActivePatPerMember = 10; public async Task> Handle(Command request, CancellationToken ct) { if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100) { return Result.Failure(Error.Problem("Pat.NameInvalid", "Name is required (1-100 chars).")); } var activeCount = await db.ApiPersonalAccessToken .CountAsync(c => c.OwnerMemberID == request.OwnerMemberID && c.RevokedAt == null, ct); if (activeCount >= MaxActivePatPerMember) { return Result.Failure(Error.Problem("Pat.LimitExceeded", $"Max {MaxActivePatPerMember} active PAT per member.")); } var token = ApiTokenGenerator.GeneratePat(); var scopesCsv = string.Join(",", (request.Scopes ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim())); var pat = ApiPersonalAccessToken.Create( request.OwnerMemberID, request.Name, ApiTokenGenerator.Hash(token), ApiTokenGenerator.Hint(token), scopesCsv, request.ExpiresAt ); await db.ApiPersonalAccessToken.AddAsync(pat, ct); await db.SaveChangesAsync(ct); return Result.Success(new Response( pat.ID, pat.Name, token, pat.TokenHint, scopesCsv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), pat.ExpiresAt, pat.CreatedAt )); } }