Multi-tenancy hardening: explicit companyId on all typed repository methods
All typed repository methods that previously relied solely on global query filters now require an explicit companyId parameter, providing defense-in- depth so IgnoreQueryFilters calls cannot leak cross-tenant data. - IBillRepository/BillRepository: GetForIndexAsync, LoadForViewAsync, LoadForEditAsync, GetLastBillNumberAsync, GetLastPaymentNumberAsync, GetForDateRangeAsync all scoped to companyId - IJobRepository/JobRepository: LoadForDetailsAsync, LoadForEditAsync, LoadForStatusChangeAsync, GetChangeHistoryAsync, LoadForTemplateSnapshotAsync, GetReworkJobCountAsync - IQuoteRepository/QuoteRepository: LoadForDetailsAsync, GetChangeHistoryAsync, GetItemsWithCoatsAsync - IInvoiceRepository/InvoiceRepository: LoadForViewAsync - ICustomerRepository/CustomerRepository: LoadForDetailsAsync - INotificationLogRepository/NotificationLogRepository: all 6 FK methods - BillsController: ITenantContext injected, all call sites updated - AccountingExportController, InvoicesController, JobsController, JobTemplatesController, QuotesController: call sites updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -288,8 +288,9 @@ public class QuotesController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
// Load quote with all navigations needed for the Details view
|
||||
var quote = await _unitOfWork.Quotes.LoadForDetailsAsync(id.Value);
|
||||
var quote = await _unitOfWork.Quotes.LoadForDetailsAsync(id.Value, companyId);
|
||||
|
||||
if (quote == null)
|
||||
{
|
||||
@@ -412,7 +413,7 @@ public class QuotesController : Controller
|
||||
};
|
||||
|
||||
// Load change history
|
||||
var changeHistories = await _unitOfWork.Quotes.GetChangeHistoryAsync(id.Value);
|
||||
var changeHistories = await _unitOfWork.Quotes.GetChangeHistoryAsync(id.Value, companyId);
|
||||
var changeHistoryDtos = _mapper.Map<List<QuoteChangeHistoryDto>>(changeHistories);
|
||||
ViewBag.ChangeHistory = changeHistoryDtos;
|
||||
|
||||
@@ -560,7 +561,7 @@ public class QuotesController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value);
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value, _tenantContext.GetCurrentCompanyId() ?? 0);
|
||||
quoteDto.QuoteItems = _mapper.Map<List<QuoteItemDto>>(quoteItems);
|
||||
|
||||
// Get company info and logo
|
||||
@@ -1082,7 +1083,7 @@ public class QuotesController : Controller
|
||||
_logger.LogWarning(ex, "Notification failed for quote {Id}", quote.Id);
|
||||
}
|
||||
|
||||
var quoteCreateNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(quote.Id);
|
||||
var quoteCreateNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(quote.Id, currentUser!.CompanyId);
|
||||
this.SetNotificationResultToast(quoteCreateNotifLog);
|
||||
}
|
||||
|
||||
@@ -1135,7 +1136,7 @@ public class QuotesController : Controller
|
||||
}
|
||||
|
||||
// Get quote items with their coats, prep services and catalog item
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value);
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value, _tenantContext.GetCurrentCompanyId() ?? 0);
|
||||
|
||||
_logger.LogInformation("=== LOADING QUOTE {QuoteId} FOR EDIT ===", id.Value);
|
||||
foreach (var item in quoteItems)
|
||||
@@ -2197,7 +2198,7 @@ public class QuotesController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value);
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value, _tenantContext.GetCurrentCompanyId() ?? 0);
|
||||
quoteDto.QuoteItems = _mapper.Map<List<QuoteItemDto>>(quoteItems);
|
||||
|
||||
// Warn on confirmation page if a job is linked
|
||||
@@ -2356,7 +2357,7 @@ public class QuotesController : Controller
|
||||
_logger.LogWarning(ex, "Notification failed for quote {Id}", id);
|
||||
}
|
||||
|
||||
var approveNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id);
|
||||
var approveNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id, quote.CompanyId);
|
||||
this.SetNotificationResultToast(approveNotifLog);
|
||||
}
|
||||
|
||||
@@ -2718,7 +2719,7 @@ public class QuotesController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quoteId);
|
||||
var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quoteId, currentUser.CompanyId);
|
||||
quoteDto.QuoteItems = _mapper.Map<List<QuoteItemDto>>(quoteItems);
|
||||
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(currentUser.CompanyId);
|
||||
@@ -2892,7 +2893,7 @@ public class QuotesController : Controller
|
||||
|
||||
// Always reload quote items with full coat/prep-service data so this works
|
||||
// regardless of which caller loaded the quote (some callers don't include coats).
|
||||
var fullItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quote.Id);
|
||||
var fullItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quote.Id, quote.CompanyId);
|
||||
// Do NOT assign fullItems to quote.QuoteItems — quote is a tracked entity and assigning
|
||||
// no-tracking children (which may share InventoryItem instances) causes EF identity conflicts.
|
||||
|
||||
@@ -3210,7 +3211,7 @@ public class QuotesController : Controller
|
||||
await _notificationService.NotifyQuoteSentAsync(quote, pdfBytes, pdfFilename, trimmedOverride);
|
||||
|
||||
// Check the most recent log entry to get actual send status
|
||||
var latestLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id);
|
||||
var latestLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id, quote.CompanyId);
|
||||
|
||||
if (latestLog?.Status == NotificationStatus.Failed)
|
||||
return Json(new { success = false, message = $"Email delivery failed: {latestLog.ErrorMessage}" });
|
||||
@@ -3312,7 +3313,7 @@ public class QuotesController : Controller
|
||||
public async Task<IActionResult> NotificationsSent(int id)
|
||||
{
|
||||
var tz = ViewBag.CompanyTimeZone as string;
|
||||
var rawLogs = await _unitOfWork.NotificationLogs.GetAllForQuoteAsync(id);
|
||||
var rawLogs = await _unitOfWork.NotificationLogs.GetAllForQuoteAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0);
|
||||
var logs = rawLogs.Select(n => new { n.Id, Channel = n.Channel.ToString(), Type = n.NotificationType.ToString(),
|
||||
Status = n.Status.ToString(), n.RecipientName, n.Recipient, n.Subject, n.ErrorMessage,
|
||||
SentAt = n.SentAt.Tz(tz).ToString("MMM d, yyyy h:mm tt") });
|
||||
|
||||
Reference in New Issue
Block a user