using EntitlementBLL.Auth; using EntitlementSL.Common; using FastEndpoints; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.FastEndPoint; using Microsoft.AspNetCore.RateLimiting; using ErrorResponse = GB5Shared.Resource.Response.ErrorResponse; using static GB5Shared.GB5Constant.Constant; namespace EntitlementSL.Endpoints.ClientAuth; // AllowAnonymous — this IS the credential-bootstrapping endpoint; there is no caller identity to // check yet. // // Design decision (ClientId on login): MENTITLEMENTCLIENTUSER.EMAIL is unique only per // (CLIENTID, EMAIL) (see the Phase 2 schema migration), not globally — two different clients could // independently have a contact who happens to share an email address. Rather than guess/ // disambiguate server-side (or worse, silently pick an arbitrary match), the login form requires // ClientId explicitly alongside Email/Password. The FE would typically resolve this from a // subdomain or a "Company/Client Code" field — that's Surface B's concern, not this endpoint's. // // Failure handling: wrong email, wrong password, and a not-Active account all collapse to the // exact same generic rejection (never leaking which reason applied) — mirrors RefreshToken.cs's // posture on Invalid vs ReuseDetected, applied here as ordinary login-security hygiene. public class Login : BaseEndpoint> { private readonly IClientAuthBLL _ClientAuthBLL; public Login(IClientAuthBLL clientAuthBLL) => _ClientAuthBLL = clientAuthBLL; public override void Configure() { Post("/lic/ClientAuth.svc/Login"); AllowAnonymous(); // Per-IP rate limit — see ClientAuthRateLimitOptions/Program.cs. Closes a gap DXP has // nowhere in its own auth surface: zero rate limiting anywhere. Options(x => x.RequireRateLimiting(ClientAuthRateLimitPolicies.LoginAndRefresh)); } public record Body(int ClientId, string Email, string Password); public record Params( [property: FromHeader] string Login, [property: FromBody] Body RequestBody ); protected override string? GetCacheKey(Params req, LoginDTO loginDTO) => null; protected override async Task> ExecuteAsync(Params req, LoginDTO loginDTO, CancellationToken ct) { var result = await _ClientAuthBLL.LoginAsync( req.RequestBody.ClientId, req.RequestBody.Email, req.RequestBody.Password, loginDTO, ct); if (result.Status != ClientLoginResultStatus.Success || result.TokenPair is null) return await GB5Shared.ResponseStandard.Response.CreateErrorResponse( ErrorResponse.InvalidCredentialsMessage, CacheKeyLevel.NOT_REQUIRED, loginDTO, statusCode: 401); return await GB5Shared.ResponseStandard.Response.CreateSuccessResponse( ClientTokenResponse.From(result.TokenPair, result.PendingAgreementVersionIds), CacheKeyLevel.NOT_REQUIRED, loginDTO); } }