using System.Collections.Generic; using System.IO; using System.Linq; using Dapper; using FastEndpoints; using GB5Shared.Connection; using GB5Shared.DTO.Framework.Enum; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.QueryExecutor; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using PartnerDAL.Query.PartnerBrand; namespace PartnerSL.EndPoints.PartnerBrand { // Public endpoint — no Login header required. // Serves a partner's logo/dark-logo/favicon as the raw image file (correct // Content-Type, no JSON/Base64 wrapper) — open this URL directly in a browser // to view it, with no truncation or encoding to worry about. // Same two-hop resolution and PartnerId override as GetPublicBrandByConnection. public class GetPartnerLogoFile : Endpoint { private readonly IApplicationConnection _appConnection; private readonly IQueryExecutor _queryExecutor; private readonly ILogger _logger; public GetPartnerLogoFile(IApplicationConnection appConnection, IQueryExecutor queryExecutor, ILogger logger) { _appConnection = appConnection; _queryExecutor = queryExecutor; _logger = logger; } public override void Configure() { Get("/PartnerBrand/GetPartnerLogoFile"); AllowAnonymous(); } public class GetPartnerLogoFileRequest { [QueryParam] public string ConnectionName { get; set; } = ""; [QueryParam] public int? PartnerId { get; set; } // Logo | LogoDark | Favicon — which of the three files to serve. Defaults to Logo. [QueryParam] public string Type { get; set; } = "Logo"; } public override async Task HandleAsync(GetPartnerLogoFileRequest req, CancellationToken ct) { try { await HandleCoreAsync(req, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetPartnerLogoFile failed for ConnectionName='{ConnectionName}' PartnerId={PartnerId} Type={Type}.", req.ConnectionName, req.PartnerId, req.Type); await Send.ResponseAsync(new ResponseStandardDTO { Body = "", ErrorBody = "Logo resolution failed — the connection's database may be missing the Partner Platform schema, or is unreachable. Check server logs for details.", Status = FrameworkEnumDTO.ResponseStatus.Failed }, 500, ct); } } private async Task HandleCoreAsync(GetPartnerLogoFileRequest req, CancellationToken ct) { if (string.IsNullOrWhiteSpace(req.ConnectionName)) { await Send.ResponseAsync(new ResponseStandardDTO { Body = "", ErrorBody = "ConnectionName is required.", Status = FrameworkEnumDTO.ResponseStatus.Failed }, 400, ct); return; } var connectionName = req.ConnectionName.Trim(); // ── Hop 1 — GB5System: resolve routing + PartnerProductId ────────── string systemConnStr = await _appConnection.Gb5SystemConnectionString().ConfigureAwait(false); using var systemConn = new SqlConnection(systemConnStr); var routing = (await systemConn.QueryAsync( PartnerBrandQB.GET_ROUTING_BY_CONNECTION_NAME, new { ConnectionName = connectionName }).ConfigureAwait(false)).AsList(); if (routing.Count != 1) { await Send.ResponseAsync(new ResponseStandardDTO { Body = "", ErrorBody = routing.Count == 0 ? "No active server configuration found for the given connection name." : "More than one active server configuration found for the given connection name.", Status = FrameworkEnumDTO.ResponseStatus.Failed }, routing.Count == 0 ? 404 : 409, ct); return; } int partnerProductId = (int)routing[0].PartnerProductId; if (req.PartnerId.HasValue && req.PartnerId.Value > 0) { var overrideRows = await _queryExecutor.QueryAsync( connectionName, PartnerBrandQB.GET_PARTNERPRODUCTID_BY_PARTNER_ID, new { PartnerId = req.PartnerId.Value }).ConfigureAwait(false); var overrideRow = overrideRows.FirstOrDefault(); if (overrideRow == null) { await Send.ResponseAsync(new ResponseStandardDTO { Body = "", ErrorBody = $"No active PartnerProduct found for PartnerId {req.PartnerId.Value} in this connection's database.", Status = FrameworkEnumDTO.ResponseStatus.Failed }, 404, ct); return; } partnerProductId = (int)overrideRow.PARTNERPRODUCTID; } if (partnerProductId <= 0) { await Send.ResponseAsync(new ResponseStandardDTO { Body = "", ErrorBody = "This client has no partner assigned — no logo to serve.", Status = FrameworkEnumDTO.ResponseStatus.Failed }, 404, ct); return; } // ── Hop 2 — the tenant's own ProductionDB, resolved by ConnectionName ── var brandRows = await _queryExecutor.QueryAsync( connectionName, PartnerBrandQB.GET_TENANT_LOCAL_PUBLIC_BRAND, new { PartnerProductId = partnerProductId, BaseUri = "" }).ConfigureAwait(false); var brand = (IDictionary)brandRows.FirstOrDefault(); if (brand == null) { await Send.ResponseAsync(new ResponseStandardDTO { Body = "", ErrorBody = "No brand found for this partner product.", Status = FrameworkEnumDTO.ResponseStatus.Failed }, 404, ct); return; } string urlKey = req.Type?.Trim().ToLowerInvariant() switch { "logodark" => "LogoDarkFileUrl", "favicon" => "FaviconFileUrl", _ => "LogoFileUrl" }; brand.TryGetValue(urlKey, out var pathObj); var path = pathObj as string; if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) { await Send.ResponseAsync(new ResponseStandardDTO { Body = "", ErrorBody = $"File not found for {req.Type} (path: {path}).", Status = FrameworkEnumDTO.ResponseStatus.Failed }, 404, ct); return; } var mime = Path.GetExtension(path).ToLowerInvariant() switch { ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".gif" => "image/gif", ".svg" => "image/svg+xml", ".webp" => "image/webp", ".ico" => "image/x-icon", _ => "application/octet-stream" }; var bytes = await File.ReadAllBytesAsync(path, ct); await Send.BytesAsync(bytes, contentType: mime, cancellation: ct); } } }