using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using SeaHaven.Services.Interfaces; namespace Api.SeaHavenIndustries.Infrastructure { public class FileStorageAdapter : IFileStoragePort { private readonly IWebHostEnvironment _webHostEnvironment; private readonly IHttpContextAccessor _httpContext; public FileStorageAdapter(IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext) { _webHostEnvironment = webHostEnvironment; _httpContext = httpContext; } public async Task SaveFileAsync(IFormFile file) { var fileName = Path.GetFileName(file.FileName); if (string.IsNullOrWhiteSpace(fileName)) throw new InvalidDataException("The uploaded file must have a valid name."); var uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; var uploadPath = Path.Combine("Assets", "Documents"); var webRoot = ResolveWebRoot(); var fullPath = Path.Combine(webRoot, uploadPath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); using (var stream = System.IO.File.Create(fullPath)) { await file.CopyToAsync(stream); } var request = _httpContext.HttpContext?.Request ?? throw new InvalidOperationException("An active HTTP request is required to build the file URL."); var domain = $"{request.Scheme}://{request.Host}"; return $"{domain}/{uploadPath.Replace("\\", "/")}/{uniqueFileName}"; } public bool TryDelete(string fileUrl) { if (string.IsNullOrWhiteSpace(fileUrl)) return false; try { if (!TryResolveDocumentPath(fileUrl, out var fullPath)) return false; if (!System.IO.File.Exists(fullPath)) return false; System.IO.File.Delete(fullPath); return true; } catch { return false; } } public Stream? OpenRead(string fileUrl) { if (string.IsNullOrWhiteSpace(fileUrl)) return null; try { if (!TryResolveDocumentPath(fileUrl, out var fullPath)) return null; return System.IO.File.Exists(fullPath) ? System.IO.File.OpenRead(fullPath) : null; } catch { return null; } } private bool TryResolveDocumentPath(string fileUrl, out string fullPath) { fullPath = string.Empty; if (!Uri.TryCreate(fileUrl, UriKind.Absolute, out var uri)) return false; var relativePath = Uri.UnescapeDataString(uri.AbsolutePath).TrimStart('/'); if (string.IsNullOrWhiteSpace(relativePath) || relativePath.Contains("..", StringComparison.Ordinal) || !relativePath.StartsWith("Assets/Documents/", StringComparison.OrdinalIgnoreCase)) { return false; } var webRoot = Path.GetFullPath(ResolveWebRoot()); var documentsRoot = Path.GetFullPath(Path.Combine(webRoot, "Assets", "Documents")) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; var candidatePath = Path.GetFullPath( Path.Combine(webRoot, relativePath.Replace('/', Path.DirectorySeparatorChar))); if (!candidatePath.StartsWith(documentsRoot, StringComparison.OrdinalIgnoreCase)) return false; fullPath = candidatePath; return true; } private string ResolveWebRoot() => _webHostEnvironment.WebRootPath ?? Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot"); } public class DispatchEmailAdapter : IDispatchEmailPort { private readonly Helper.SendMessage _sendMessage; public DispatchEmailAdapter(Helper.SendMessage sendMessage) { _sendMessage = sendMessage; } public async Task SendDispatchEmailAsync(string emailTo, string subject, string htmlBody, string? replyTo) { return await _sendMessage.SendDispatchEmail(emailTo, subject, htmlBody, replyTo); } } public class VendorTokenAdapter : IVendorTokenPort { private readonly IVendorPortalTokenService _tokenService; public VendorTokenAdapter(IVendorPortalTokenService tokenService) { _tokenService = tokenService; } public async Task GetOrCreateActiveTokenAsync(int vendorId) { var token = await _tokenService.GetOrCreateActiveTokenAsync( vendorId, CancellationToken.None); return token.Token; } } public class VendorDocumentStorageAdapter : IVendorDocumentStoragePort { private readonly IWebHostEnvironment _environment; public VendorDocumentStorageAdapter(IWebHostEnvironment environment) { _environment = environment; } public async Task SaveAsync(int vendorId, int dispatchId, string storedFileName, Stream content, CancellationToken cancellationToken) { var path = Helper.VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, vendorId, dispatchId, storedFileName); Directory.CreateDirectory(Path.GetDirectoryName(path)!); await using var file = File.Create(path); await content.CopyToAsync(file, cancellationToken); } public Stream OpenRead(int vendorId, int dispatchId, string storedFileName) { var path = Helper.VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, vendorId, dispatchId, storedFileName); return File.OpenRead(path); } public void Delete(int vendorId, int dispatchId, string storedFileName) { var path = Helper.VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, vendorId, dispatchId, storedFileName); if (File.Exists(path)) { File.Delete(path); } } } }