1
0
mirror of https://github.com/bitwarden/server synced 2026-01-05 01:53:17 +00:00

Organization report tables, repos, services, and endpoints (#6158)

* PM-23754 initial commit

* pm-23754 fixing controller tests

* pm-23754 adding commands and queries

* pm-23754 adding endpoints, command/queries, repositories, and sql migrations

* pm-23754 add new sql scripts

* PM-23754 adding sql scripts

* pm-23754

* PM-23754 fixing migration script

* PM-23754 fixing migration script again

* PM-23754 fixing migration script validation

* PM-23754 fixing db validation script issue

* PM-23754 fixing endpoint and db validation

* PM-23754 fixing unit tests

* PM-23754 fixing implementation based on comments and tests

* PM-23754 updating logging statements

* PM-23754 making changes based on PR comments.

* updating migration scripts

* removing old migration files

* update code based testing for whole data object for OrganizationReport and add a stored procedure.

* updating services, unit tests, repository tests

* fixing unit tests

* fixing migration script

* fixing migration script again

* fixing migration script

* another fix

* fixing sql file, updating controller to account for different orgIds in the url and body.

* updating error message in controllers without a body

* making a change to the command

* Refactor ReportsController by removing organization reports

The IDropOrganizationReportCommand is no longer needed

* will code based on PR comments.

* fixing unit test

* fixing migration script based on last changes.

* adding another check in endpoint and adding unit tests

* fixing route parameter.

* PM-23754 updating data fields to return just the column

* PM-23754 fixing repository method signatures

* PM-23754 making change to orgId parameter through out code to align with api naming

---------

Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com>
This commit is contained in:
Graham Walker
2025-09-08 15:06:13 -05:00
committed by GitHub
parent cb0d5a5ba6
commit 226f274a72
79 changed files with 24744 additions and 1047 deletions

View File

@@ -0,0 +1,297 @@
using Bit.Core.Context;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Dirt.Controllers;
[Route("reports/organizations")]
[Authorize("Application")]
public class OrganizationReportsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IGetOrganizationReportQuery _getOrganizationReportQuery;
private readonly IAddOrganizationReportCommand _addOrganizationReportCommand;
private readonly IUpdateOrganizationReportCommand _updateOrganizationReportCommand;
private readonly IUpdateOrganizationReportSummaryCommand _updateOrganizationReportSummaryCommand;
private readonly IGetOrganizationReportSummaryDataQuery _getOrganizationReportSummaryDataQuery;
private readonly IGetOrganizationReportSummaryDataByDateRangeQuery _getOrganizationReportSummaryDataByDateRangeQuery;
private readonly IGetOrganizationReportDataQuery _getOrganizationReportDataQuery;
private readonly IUpdateOrganizationReportDataCommand _updateOrganizationReportDataCommand;
private readonly IGetOrganizationReportApplicationDataQuery _getOrganizationReportApplicationDataQuery;
private readonly IUpdateOrganizationReportApplicationDataCommand _updateOrganizationReportApplicationDataCommand;
public OrganizationReportsController(
ICurrentContext currentContext,
IGetOrganizationReportQuery getOrganizationReportQuery,
IAddOrganizationReportCommand addOrganizationReportCommand,
IUpdateOrganizationReportCommand updateOrganizationReportCommand,
IUpdateOrganizationReportSummaryCommand updateOrganizationReportSummaryCommand,
IGetOrganizationReportSummaryDataQuery getOrganizationReportSummaryDataQuery,
IGetOrganizationReportSummaryDataByDateRangeQuery getOrganizationReportSummaryDataByDateRangeQuery,
IGetOrganizationReportDataQuery getOrganizationReportDataQuery,
IUpdateOrganizationReportDataCommand updateOrganizationReportDataCommand,
IGetOrganizationReportApplicationDataQuery getOrganizationReportApplicationDataQuery,
IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand
)
{
_currentContext = currentContext;
_getOrganizationReportQuery = getOrganizationReportQuery;
_addOrganizationReportCommand = addOrganizationReportCommand;
_updateOrganizationReportCommand = updateOrganizationReportCommand;
_updateOrganizationReportSummaryCommand = updateOrganizationReportSummaryCommand;
_getOrganizationReportSummaryDataQuery = getOrganizationReportSummaryDataQuery;
_getOrganizationReportSummaryDataByDateRangeQuery = getOrganizationReportSummaryDataByDateRangeQuery;
_getOrganizationReportDataQuery = getOrganizationReportDataQuery;
_updateOrganizationReportDataCommand = updateOrganizationReportDataCommand;
_getOrganizationReportApplicationDataQuery = getOrganizationReportApplicationDataQuery;
_updateOrganizationReportApplicationDataCommand = updateOrganizationReportApplicationDataCommand;
}
#region Whole OrganizationReport Endpoints
[HttpGet("{organizationId}/latest")]
public async Task<IActionResult> GetLatestOrganizationReportAsync(Guid organizationId)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
var latestReport = await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(organizationId);
return Ok(latestReport);
}
[HttpGet("{organizationId}/{reportId}")]
public async Task<IActionResult> GetOrganizationReportAsync(Guid organizationId, Guid reportId)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);
if (report == null)
{
throw new NotFoundException("Report not found for the specified organization.");
}
if (report.OrganizationId != organizationId)
{
throw new BadRequestException("Invalid report ID");
}
return Ok(report);
}
[HttpPost("{organizationId}")]
public async Task<IActionResult> CreateOrganizationReportAsync(Guid organizationId, [FromBody] AddOrganizationReportRequest request)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
if (request.OrganizationId != organizationId)
{
throw new BadRequestException("Organization ID in the request body must match the route parameter");
}
var report = await _addOrganizationReportCommand.AddOrganizationReportAsync(request);
return Ok(report);
}
[HttpPatch("{organizationId}/{reportId}")]
public async Task<IActionResult> UpdateOrganizationReportAsync(Guid organizationId, [FromBody] UpdateOrganizationReportRequest request)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
if (request.OrganizationId != organizationId)
{
throw new BadRequestException("Organization ID in the request body must match the route parameter");
}
var updatedReport = await _updateOrganizationReportCommand.UpdateOrganizationReportAsync(request);
return Ok(updatedReport);
}
#endregion
# region SummaryData Field Endpoints
[HttpGet("{organizationId}/data/summary")]
public async Task<IActionResult> GetOrganizationReportSummaryDataByDateRangeAsync(
Guid organizationId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
if (organizationId.Equals(null))
{
throw new BadRequestException("Organization ID is required.");
}
var summaryDataList = await _getOrganizationReportSummaryDataByDateRangeQuery
.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
return Ok(summaryDataList);
}
[HttpGet("{organizationId}/data/summary/{reportId}")]
public async Task<IActionResult> GetOrganizationReportSummaryAsync(Guid organizationId, Guid reportId)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
var summaryData =
await _getOrganizationReportSummaryDataQuery.GetOrganizationReportSummaryDataAsync(organizationId, reportId);
if (summaryData == null)
{
throw new NotFoundException("Report not found for the specified organization.");
}
return Ok(summaryData);
}
[HttpPatch("{organizationId}/data/summary/{reportId}")]
public async Task<IActionResult> UpdateOrganizationReportSummaryAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportSummaryRequest request)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
if (request.OrganizationId != organizationId)
{
throw new BadRequestException("Organization ID in the request body must match the route parameter");
}
if (request.ReportId != reportId)
{
throw new BadRequestException("Report ID in the request body must match the route parameter");
}
var updatedReport = await _updateOrganizationReportSummaryCommand.UpdateOrganizationReportSummaryAsync(request);
return Ok(updatedReport);
}
#endregion
#region ReportData Field Endpoints
[HttpGet("{organizationId}/data/report/{reportId}")]
public async Task<IActionResult> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
var reportData = await _getOrganizationReportDataQuery.GetOrganizationReportDataAsync(organizationId, reportId);
if (reportData == null)
{
throw new NotFoundException("Organization report data not found.");
}
return Ok(reportData);
}
[HttpPatch("{organizationId}/data/report/{reportId}")]
public async Task<IActionResult> UpdateOrganizationReportDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportDataRequest request)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
if (request.OrganizationId != organizationId)
{
throw new BadRequestException("Organization ID in the request body must match the route parameter");
}
if (request.ReportId != reportId)
{
throw new BadRequestException("Report ID in the request body must match the route parameter");
}
var updatedReport = await _updateOrganizationReportDataCommand.UpdateOrganizationReportDataAsync(request);
return Ok(updatedReport);
}
#endregion
#region ApplicationData Field Endpoints
[HttpGet("{organizationId}/data/application/{reportId}")]
public async Task<IActionResult> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId)
{
try
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
var applicationData = await _getOrganizationReportApplicationDataQuery.GetOrganizationReportApplicationDataAsync(organizationId, reportId);
if (applicationData == null)
{
throw new NotFoundException("Organization report application data not found.");
}
return Ok(applicationData);
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
throw;
}
}
[HttpPatch("{organizationId}/data/application/{reportId}")]
public async Task<IActionResult> UpdateOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportApplicationDataRequest request)
{
try
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}
if (request.OrganizationId != organizationId)
{
throw new BadRequestException("Organization ID in the request body must match the route parameter");
}
if (request.Id != reportId)
{
throw new BadRequestException("Report ID in the request body must match the route parameter");
}
var updatedReport = await _updateOrganizationReportApplicationDataCommand.UpdateOrganizationReportApplicationDataAsync(request);
return Ok(updatedReport);
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
throw;
}
}
#endregion
}

View File

@@ -25,7 +25,6 @@ public class ReportsController : Controller
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
private readonly IAddOrganizationReportCommand _addOrganizationReportCommand;
private readonly IDropOrganizationReportCommand _dropOrganizationReportCommand;
private readonly IGetOrganizationReportQuery _getOrganizationReportQuery;
private readonly ILogger<ReportsController> _logger;
@@ -38,7 +37,6 @@ public class ReportsController : Controller
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand,
IGetOrganizationReportQuery getOrganizationReportQuery,
IAddOrganizationReportCommand addOrganizationReportCommand,
IDropOrganizationReportCommand dropOrganizationReportCommand,
ILogger<ReportsController> logger
)
{
@@ -50,7 +48,6 @@ public class ReportsController : Controller
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
_getOrganizationReportQuery = getOrganizationReportQuery;
_addOrganizationReportCommand = addOrganizationReportCommand;
_dropOrganizationReportCommand = dropOrganizationReportCommand;
_logger = logger;
}
@@ -209,195 +206,4 @@ public class ReportsController : Controller
await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request);
}
/// <summary>
/// Adds a new organization report
/// </summary>
/// <param name="request">A single instance of AddOrganizationReportRequest</param>
/// <returns>A single instance of OrganizationReport</returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
[HttpPost("organization-reports")]
public async Task<OrganizationReport> AddOrganizationReport([FromBody] AddOrganizationReportRequest request)
{
if (!await _currentContext.AccessReports(request.OrganizationId))
{
throw new NotFoundException();
}
return await _addOrganizationReportCommand.AddOrganizationReportAsync(request);
}
/// <summary>
/// Drops organization reports for an organization
/// </summary>
/// <param name="request">A single instance of DropOrganizationReportRequest</param>
/// <returns></returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization does not have any records</exception>
[HttpDelete("organization-reports")]
public async Task DropOrganizationReport([FromBody] DropOrganizationReportRequest request)
{
if (!await _currentContext.AccessReports(request.OrganizationId))
{
throw new NotFoundException();
}
await _dropOrganizationReportCommand.DropOrganizationReportAsync(request);
}
/// <summary>
/// Gets organization reports for an organization
/// </summary>
/// <param name="orgId">A valid Organization Id</param>
/// <returns>An Enumerable of OrganizationReport</returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
[HttpGet("organization-reports/{orgId}")]
public async Task<IEnumerable<OrganizationReport>> GetOrganizationReports(Guid orgId)
{
if (!await _currentContext.AccessReports(orgId))
{
throw new NotFoundException();
}
return await _getOrganizationReportQuery.GetOrganizationReportAsync(orgId);
}
/// <summary>
/// Gets the latest organization report for an organization
/// </summary>
/// <param name="orgId">A valid Organization Id</param>
/// <returns>A single instance of OrganizationReport</returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
[HttpGet("organization-reports/latest/{orgId}")]
public async Task<OrganizationReport> GetLatestOrganizationReport(Guid orgId)
{
if (!await _currentContext.AccessReports(orgId))
{
throw new NotFoundException();
}
return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId);
}
/// <summary>
/// Gets the Organization Report Summary for an organization.
/// This includes the latest report's encrypted data, encryption key, and date.
/// This is a mock implementation and should be replaced with actual data retrieval logic.
/// </summary>
/// <param name="orgId"></param>
/// <param name="from">Min date (example: 2023-01-01)</param>
/// <param name="to">Max date (example: 2023-12-31)</param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
[HttpGet("organization-report-summary/{orgId}")]
public IEnumerable<OrganizationReportSummaryModel> GetOrganizationReportSummary(
[FromRoute] Guid orgId,
[FromQuery] DateOnly from,
[FromQuery] DateOnly to)
{
if (!ModelState.IsValid)
{
throw new BadRequestException(ModelState);
}
GuardOrganizationAccess(orgId);
// FIXME: remove this mock class when actual data retrieval is implemented
return MockOrganizationReportSummary.GetMockData()
.Where(_ => _.OrganizationId == orgId
&& _.Date >= from.ToDateTime(TimeOnly.MinValue)
&& _.Date <= to.ToDateTime(TimeOnly.MaxValue));
}
/// <summary>
/// Creates a new Organization Report Summary for an organization.
/// This is a mock implementation and should be replaced with actual creation logic.
/// </summary>
/// <param name="model"></param>
/// <returns>Returns 204 Created with the created OrganizationReportSummaryModel</returns>
/// <exception cref="NotFoundException"></exception>
[HttpPost("organization-report-summary")]
public IActionResult CreateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model)
{
if (!ModelState.IsValid)
{
throw new BadRequestException(ModelState);
}
GuardOrganizationAccess(model.OrganizationId);
// TODO: Implement actual creation logic
// Returns 204 No Content as a placeholder
return NoContent();
}
[HttpPut("organization-report-summary")]
public IActionResult UpdateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model)
{
if (!ModelState.IsValid)
{
throw new BadRequestException(ModelState);
}
GuardOrganizationAccess(model.OrganizationId);
// TODO: Implement actual update logic
// Returns 204 No Content as a placeholder
return NoContent();
}
private void GuardOrganizationAccess(Guid organizationId)
{
if (!_currentContext.AccessReports(organizationId).Result)
{
throw new NotFoundException();
}
}
// FIXME: remove this mock class when actual data retrieval is implemented
private class MockOrganizationReportSummary
{
public static List<OrganizationReportSummaryModel> GetMockData()
{
return new List<OrganizationReportSummaryModel>
{
new OrganizationReportSummaryModel
{
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
EncryptedData = "2.EtCcxDEBoF1MYChYHC4Q1w==|RyZ07R7qEFBbc/ICLFpEMockL9K+PD6rOod6DGHHrkaRLHUDqDwmxbu3jnD0cg8s7GIYmp0jApHXC+82QdApk87pA0Kr8fN2Rj0+8bDQCjhKfoRTipAB25S/n2E+ttjvlFfag92S66XqUH9S/eZw/Q==|0bPfykHk3SqS/biLNcNoYtH6YTstBEKu3AhvdZZLxhU=",
EncryptionKey = "2.Dd/TtdNwxWdYg9+fRkxh6w==|8KAiK9SoadgFRmyVOchd4tNh2vErD1Rv9x1gqtsE5tzxKE/V/5kkr1WuVG+QpEj//YaQt221UEMESRSXicZ7a9cB6xXLBkbbFwmecQRJVBs=|902em44n9cwciZzYrYuX6MRzRa+4hh1HHfNAxyJx/IM=",
Date = DateTime.UtcNow
},
new OrganizationReportSummaryModel
{
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
EncryptedData = "2.HvY4fAvbzYV1hqa3255m5Q==|WcKga2Wka5i8fVso8MgjzfBAwxaqdhZDL3bnvhDsisZ0r9lNKQcG3YUQSFpJxr74cgg5QRQaFieCUe2YppciHDT6bsaE2VzFce3cNNB821uTFqnlJClkGJpG1nGvPupdErrg4Ik57WenEzYesmR4pw==|F0aJfF+1MlPm+eAlQnDgFnwfv198N9VtPqFJa4+UFqk=",
EncryptionKey = "2.ctMgLN4ycPusbQArG/uiag==|NtqiQsAoUxMSTBQsxAMyVLWdt5lVEUGZQNxZSBU4l76ywH2f6dx5FWFrcF3t3GBqy5yDoc5eBg0VlJDW9coqzp8j9n8h1iMrtmXPyBMAhbc=|pbH+w68BUdUKYCfNRpjd8NENw2lZ0vfxgMuTrsrRCTQ=",
Date = DateTime.UtcNow.AddMonths(-1)
},
new OrganizationReportSummaryModel
{
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
EncryptedData = "2.NH4qLZYUkz/+qpB/mRsLTA==|LEFt05jJz0ngh+Hl5lqk6kebj7lZMefA3eFdL1kLJSGdD3uTOngRwH7GXLQNFeQOxutnLX9YUILbUEPwaM8gCwNQ1KWYdB1Z+Ky4nzKRb60N7L5aTA2za6zXTIdjv7Zwhg0jPZ6sPevTuvSyqjMCuA==|Uuu6gZaF0wvB2mHFwtvHegMxfe8DgsYWTRfGiVn4lkM=",
EncryptionKey = "2.3YwG78ykSxAn44NcymdG4w==|4jfn0nLoFielicAFbmq27DNUUjV4SwGePnjYRmOa7hk4pEPnQRS3MsTJFbutVyXOgKFY9Yn2yGFZownY9EmXOMM+gHPD0t6TfzUKqQcRyuI=|wasP9zZEL9mFH5HzJYrMxnKUr/XlFKXCxG9uW66uaPU=",
Date = DateTime.UtcNow.AddMonths(-1)
},
new OrganizationReportSummaryModel
{
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
EncryptedData = "2.YmKWj/707wDPONh+JXPBOw==|Fx4jcUHmnUnSMCU8vdThMSYpDyKPnC09TxpSbNxia0M6MFbd5WHElcVribrYgTENyU0HlqPW43hThJ6xXCM0EjEWP7/jb/0l07vMNkA7sDYq+czf0XnYZgZSGKh06wFVz8xkhaPTdsiO4CXuMsoH+w==|DDVwVFHzdfbPQe3ycCx82eYVHDW97V/eWTPsNpHX/+U=",
EncryptionKey = "2.f/U45I7KF+JKfnvOArUyaw==|zNhhS2q2WwBl6SqLWMkxrXC8EX91Ra9LJExywkJhsRbxubRLt7fK+YWc8T1LUaDmMwJ3G8buSPGzyacKX0lnUR33dW6DIaLNgRZ/ekb/zkg=|qFoIZWwS0foiiIOyikFRwQKmmmI2HeyHcOVklJnIILI=",
Date = DateTime.UtcNow.AddMonths(-1)
},
new OrganizationReportSummaryModel
{
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
EncryptedData = "2.WYauwooJUEY3kZsDPphmrA==|oguYW6h10A4GxK4KkRS0X32qSTekU2CkGqNDNGfisUgvJzsyoVTafO9sVcdPdg4BUM7YNkPMjYiKEc5jMHkIgLzbnM27jcGvMJrrccSrLHiWL6/mEiqQkV3TlfiZF9i3wqj1ITsYRzM454uNle6Wrg==|uR67aFYb1i5LSidWib0iTf8091l8GY5olHkVXse3CAw=",
EncryptionKey = "2.ZyV9+9A2cxNaf8dfzfbnlA==|hhorBpVkcrrhTtNmd6SNHYI8gPNokGLOC22Vx8Qa/AotDAcyuYWw56zsawMnzpAdJGEJFtszKM2+VUVOcroCTMWHpy8yNf/kZA6uPk3Lz3s=|ASzVeJf+K1ZB8NXuypamRBGRuRq0GUHZBEy5r/O7ORY=",
Date = DateTime.UtcNow.AddMonths(-1)
},
};
}
}
}

View File

@@ -9,12 +9,15 @@ public class OrganizationReport : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public DateTime Date { get; set; }
public string ReportData { get; set; } = string.Empty;
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public string ContentEncryptionKey { get; set; } = string.Empty;
public string? SummaryData { get; set; } = null;
public string? ApplicationData { get; set; } = null;
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();

View File

@@ -0,0 +1,6 @@
namespace Bit.Core.Dirt.Models.Data;
public class OrganizationReportApplicationDataResponse
{
public string? ApplicationData { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Bit.Core.Dirt.Models.Data;
public class OrganizationReportDataResponse
{
public string? ReportData { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Bit.Core.Dirt.Models.Data;
public class OrganizationReportSummaryDataResponse
{
public string? SummaryData { get; set; }
}

View File

@@ -26,12 +26,12 @@ public class AddOrganizationReportCommand : IAddOrganizationReportCommand
public async Task<OrganizationReport> AddOrganizationReportAsync(AddOrganizationReportRequest request)
{
_logger.LogInformation("Adding organization report for organization {organizationId}", request.OrganizationId);
_logger.LogInformation(Constants.BypassFiltersEventId, "Adding organization report for organization {organizationId}", request.OrganizationId);
var (isValid, errorMessage) = await ValidateRequestAsync(request);
if (!isValid)
{
_logger.LogInformation("Failed to add organization {organizationId} report: {errorMessage}", request.OrganizationId, errorMessage);
_logger.LogInformation(Constants.BypassFiltersEventId, "Failed to add organization {organizationId} report: {errorMessage}", request.OrganizationId, errorMessage);
throw new BadRequestException(errorMessage);
}
@@ -39,15 +39,18 @@ public class AddOrganizationReportCommand : IAddOrganizationReportCommand
{
OrganizationId = request.OrganizationId,
ReportData = request.ReportData,
Date = request.Date == default ? DateTime.UtcNow : request.Date,
CreationDate = DateTime.UtcNow,
ContentEncryptionKey = request.ContentEncryptionKey,
SummaryData = request.SummaryData,
ApplicationData = request.ApplicationData,
RevisionDate = DateTime.UtcNow
};
organizationReport.SetNewId();
var data = await _organizationReportRepo.CreateAsync(organizationReport);
_logger.LogInformation("Successfully added organization report for organization {organizationId}, {organizationReportId}",
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully added organization report for organization {organizationId}, {organizationReportId}",
request.OrganizationId, data.Id);
return data;
@@ -63,12 +66,26 @@ public class AddOrganizationReportCommand : IAddOrganizationReportCommand
return (false, "Invalid Organization");
}
// ensure that we have report data
if (string.IsNullOrWhiteSpace(request.ContentEncryptionKey))
{
return (false, "Content Encryption Key is required");
}
if (string.IsNullOrWhiteSpace(request.ReportData))
{
return (false, "Report Data is required");
}
if (string.IsNullOrWhiteSpace(request.SummaryData))
{
return (false, "Summary Data is required");
}
if (string.IsNullOrWhiteSpace(request.ApplicationData))
{
return (false, "Application Data is required");
}
return (true, string.Empty);
}
}

View File

@@ -1,45 +0,0 @@
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class DropOrganizationReportCommand : IDropOrganizationReportCommand
{
private IOrganizationReportRepository _organizationReportRepo;
private ILogger<DropOrganizationReportCommand> _logger;
public DropOrganizationReportCommand(
IOrganizationReportRepository organizationReportRepository,
ILogger<DropOrganizationReportCommand> logger)
{
_organizationReportRepo = organizationReportRepository;
_logger = logger;
}
public async Task DropOrganizationReportAsync(DropOrganizationReportRequest request)
{
_logger.LogInformation("Dropping organization report for organization {organizationId}",
request.OrganizationId);
var data = await _organizationReportRepo.GetByOrganizationIdAsync(request.OrganizationId);
if (data == null || data.Count() == 0)
{
_logger.LogInformation("No organization reports found for organization {organizationId}", request.OrganizationId);
throw new BadRequestException("No data found.");
}
data
.Where(_ => request.OrganizationReportIds.Contains(_.Id))
.ToList()
.ForEach(async reportId =>
{
_logger.LogInformation("Dropping organization report {organizationReportId} for organization {organizationId}",
reportId, request.OrganizationId);
await _organizationReportRepo.DeleteAsync(reportId);
});
}
}

View File

@@ -0,0 +1,62 @@
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class GetOrganizationReportApplicationDataQuery : IGetOrganizationReportApplicationDataQuery
{
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<GetOrganizationReportApplicationDataQuery> _logger;
public GetOrganizationReportApplicationDataQuery(
IOrganizationReportRepository organizationReportRepo,
ILogger<GetOrganizationReportApplicationDataQuery> logger)
{
_organizationReportRepo = organizationReportRepo;
_logger = logger;
}
public async Task<OrganizationReportApplicationDataResponse> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report application data for organization {organizationId} and report {reportId}",
organizationId, reportId);
if (organizationId == Guid.Empty)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportApplicationDataAsync called with empty OrganizationId");
throw new BadRequestException("OrganizationId is required.");
}
if (reportId == Guid.Empty)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportApplicationDataAsync called with empty ReportId");
throw new BadRequestException("ReportId is required.");
}
var applicationDataResponse = await _organizationReportRepo.GetApplicationDataAsync(reportId);
if (applicationDataResponse == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "No application data found for organization {organizationId} and report {reportId}",
organizationId, reportId);
throw new NotFoundException("Organization report application data not found.");
}
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved organization report application data for organization {organizationId} and report {reportId}",
organizationId, reportId);
return applicationDataResponse;
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
_logger.LogError(ex, "Error fetching organization report application data for organization {organizationId} and report {reportId}",
organizationId, reportId);
throw;
}
}
}

View File

@@ -0,0 +1,62 @@
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class GetOrganizationReportDataQuery : IGetOrganizationReportDataQuery
{
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<GetOrganizationReportDataQuery> _logger;
public GetOrganizationReportDataQuery(
IOrganizationReportRepository organizationReportRepo,
ILogger<GetOrganizationReportDataQuery> logger)
{
_organizationReportRepo = organizationReportRepo;
_logger = logger;
}
public async Task<OrganizationReportDataResponse> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report data for organization {organizationId} and report {reportId}",
organizationId, reportId);
if (organizationId == Guid.Empty)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportDataAsync called with empty OrganizationId");
throw new BadRequestException("OrganizationId is required.");
}
if (reportId == Guid.Empty)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportDataAsync called with empty ReportId");
throw new BadRequestException("ReportId is required.");
}
var reportDataResponse = await _organizationReportRepo.GetReportDataAsync(reportId);
if (reportDataResponse == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "No report data found for organization {organizationId} and report {reportId}",
organizationId, reportId);
throw new NotFoundException("Organization report data not found.");
}
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved organization report data for organization {organizationId} and report {reportId}",
organizationId, reportId);
return reportDataResponse;
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
_logger.LogError(ex, "Error fetching organization report data for organization {organizationId} and report {reportId}",
organizationId, reportId);
throw;
}
}
}

View File

@@ -19,15 +19,23 @@ public class GetOrganizationReportQuery : IGetOrganizationReportQuery
_logger = logger;
}
public async Task<IEnumerable<OrganizationReport>> GetOrganizationReportAsync(Guid organizationId)
public async Task<OrganizationReport> GetOrganizationReportAsync(Guid reportId)
{
if (organizationId == Guid.Empty)
if (reportId == Guid.Empty)
{
throw new BadRequestException("OrganizationId is required.");
throw new BadRequestException("Id of report is required.");
}
_logger.LogInformation("Fetching organization reports for organization {organizationId}", organizationId);
return await _organizationReportRepo.GetByOrganizationIdAsync(organizationId);
_logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization reports for organization by Id: {reportId}", reportId);
var results = await _organizationReportRepo.GetByIdAsync(reportId);
if (results == null)
{
throw new NotFoundException($"No report found for Id: {reportId}");
}
return results;
}
public async Task<OrganizationReport> GetLatestOrganizationReportAsync(Guid organizationId)
@@ -37,7 +45,7 @@ public class GetOrganizationReportQuery : IGetOrganizationReportQuery
throw new BadRequestException("OrganizationId is required.");
}
_logger.LogInformation("Fetching latest organization report for organization {organizationId}", organizationId);
_logger.LogInformation(Constants.BypassFiltersEventId, "Fetching latest organization report for organization {organizationId}", organizationId);
return await _organizationReportRepo.GetLatestByOrganizationIdAsync(organizationId);
}
}

View File

@@ -0,0 +1,89 @@
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class GetOrganizationReportSummaryDataByDateRangeQuery : IGetOrganizationReportSummaryDataByDateRangeQuery
{
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<GetOrganizationReportSummaryDataByDateRangeQuery> _logger;
public GetOrganizationReportSummaryDataByDateRangeQuery(
IOrganizationReportRepository organizationReportRepo,
ILogger<GetOrganizationReportSummaryDataByDateRangeQuery> logger)
{
_organizationReportRepo = organizationReportRepo;
_logger = logger;
}
public async Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetOrganizationReportSummaryDataByDateRangeAsync(Guid organizationId, DateTime startDate, DateTime endDate)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report summary data by date range for organization {organizationId}, from {startDate} to {endDate}",
organizationId, startDate, endDate);
var (isValid, errorMessage) = ValidateRequest(organizationId, startDate, endDate);
if (!isValid)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportSummaryDataByDateRangeAsync validation failed: {errorMessage}", errorMessage);
throw new BadRequestException(errorMessage);
}
IEnumerable<OrganizationReportSummaryDataResponse> summaryDataList = (await _organizationReportRepo
.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)) ??
Enumerable.Empty<OrganizationReportSummaryDataResponse>();
var resultList = summaryDataList.ToList();
if (!resultList.Any())
{
_logger.LogInformation(Constants.BypassFiltersEventId, "No summary data found for organization {organizationId} in date range {startDate} to {endDate}",
organizationId, startDate, endDate);
return Enumerable.Empty<OrganizationReportSummaryDataResponse>();
}
else
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved {count} organization report summary data records for organization {organizationId} in date range {startDate} to {endDate}",
resultList.Count, organizationId, startDate, endDate);
}
return resultList;
}
catch (Exception ex) when (!(ex is BadRequestException))
{
_logger.LogError(ex, "Error fetching organization report summary data by date range for organization {organizationId}, from {startDate} to {endDate}",
organizationId, startDate, endDate);
throw;
}
}
private static (bool IsValid, string errorMessage) ValidateRequest(Guid organizationId, DateTime startDate, DateTime endDate)
{
if (organizationId == Guid.Empty)
{
return (false, "OrganizationId is required");
}
if (startDate == default)
{
return (false, "StartDate is required");
}
if (endDate == default)
{
return (false, "EndDate is required");
}
if (startDate > endDate)
{
return (false, "StartDate must be earlier than or equal to EndDate");
}
return (true, string.Empty);
}
}

View File

@@ -0,0 +1,62 @@
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class GetOrganizationReportSummaryDataQuery : IGetOrganizationReportSummaryDataQuery
{
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<GetOrganizationReportSummaryDataQuery> _logger;
public GetOrganizationReportSummaryDataQuery(
IOrganizationReportRepository organizationReportRepo,
ILogger<GetOrganizationReportSummaryDataQuery> logger)
{
_organizationReportRepo = organizationReportRepo;
_logger = logger;
}
public async Task<OrganizationReportSummaryDataResponse> GetOrganizationReportSummaryDataAsync(Guid organizationId, Guid reportId)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report summary data for organization {organizationId} and report {reportId}",
organizationId, reportId);
if (organizationId == Guid.Empty)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportSummaryDataAsync called with empty OrganizationId");
throw new BadRequestException("OrganizationId is required.");
}
if (reportId == Guid.Empty)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportSummaryDataAsync called with empty ReportId");
throw new BadRequestException("ReportId is required.");
}
var summaryDataResponse = await _organizationReportRepo.GetSummaryDataAsync(reportId);
if (summaryDataResponse == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "No summary data found for organization {organizationId} and report {reportId}",
organizationId, reportId);
throw new NotFoundException("Organization report summary data not found.");
}
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved organization report summary data for organization {organizationId} and report {reportId}",
organizationId, reportId);
return summaryDataResponse;
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
_logger.LogError(ex, "Error fetching organization report summary data for organization {organizationId} and report {reportId}",
organizationId, reportId);
throw;
}
}
}

View File

@@ -1,9 +0,0 @@

using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IDropOrganizationReportCommand
{
Task DropOrganizationReportAsync(DropOrganizationReportRequest request);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Dirt.Models.Data;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IGetOrganizationReportApplicationDataQuery
{
Task<OrganizationReportApplicationDataResponse> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Dirt.Models.Data;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IGetOrganizationReportDataQuery
{
Task<OrganizationReportDataResponse> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId);
}

View File

@@ -4,6 +4,6 @@ namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IGetOrganizationReportQuery
{
Task<IEnumerable<OrganizationReport>> GetOrganizationReportAsync(Guid organizationId);
Task<OrganizationReport> GetOrganizationReportAsync(Guid organizationId);
Task<OrganizationReport> GetLatestOrganizationReportAsync(Guid organizationId);
}

View File

@@ -0,0 +1,9 @@
using Bit.Core.Dirt.Models.Data;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IGetOrganizationReportSummaryDataByDateRangeQuery
{
Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetOrganizationReportSummaryDataByDateRangeAsync(
Guid organizationId, DateTime startDate, DateTime endDate);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Dirt.Models.Data;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IGetOrganizationReportSummaryDataQuery
{
Task<OrganizationReportSummaryDataResponse> GetOrganizationReportSummaryDataAsync(Guid organizationId, Guid reportId);
}

View File

@@ -0,0 +1,9 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IUpdateOrganizationReportApplicationDataCommand
{
Task<OrganizationReport> UpdateOrganizationReportApplicationDataAsync(UpdateOrganizationReportApplicationDataRequest request);
}

View File

@@ -0,0 +1,9 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IUpdateOrganizationReportCommand
{
Task<OrganizationReport> UpdateOrganizationReportAsync(UpdateOrganizationReportRequest request);
}

View File

@@ -0,0 +1,9 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IUpdateOrganizationReportDataCommand
{
Task<OrganizationReport> UpdateOrganizationReportDataAsync(UpdateOrganizationReportDataRequest request);
}

View File

@@ -0,0 +1,9 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IUpdateOrganizationReportSummaryCommand
{
Task<OrganizationReport> UpdateOrganizationReportSummaryAsync(UpdateOrganizationReportSummaryRequest request);
}

View File

@@ -14,7 +14,14 @@ public static class ReportingServiceCollectionExtensions
services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();
services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>();
services.AddScoped<IAddOrganizationReportCommand, AddOrganizationReportCommand>();
services.AddScoped<IDropOrganizationReportCommand, DropOrganizationReportCommand>();
services.AddScoped<IGetOrganizationReportQuery, GetOrganizationReportQuery>();
services.AddScoped<IUpdateOrganizationReportCommand, UpdateOrganizationReportCommand>();
services.AddScoped<IUpdateOrganizationReportSummaryCommand, UpdateOrganizationReportSummaryCommand>();
services.AddScoped<IGetOrganizationReportSummaryDataQuery, GetOrganizationReportSummaryDataQuery>();
services.AddScoped<IGetOrganizationReportSummaryDataByDateRangeQuery, GetOrganizationReportSummaryDataByDateRangeQuery>();
services.AddScoped<IGetOrganizationReportDataQuery, GetOrganizationReportDataQuery>();
services.AddScoped<IUpdateOrganizationReportDataCommand, UpdateOrganizationReportDataCommand>();
services.AddScoped<IGetOrganizationReportApplicationDataQuery, GetOrganizationReportApplicationDataQuery>();
services.AddScoped<IUpdateOrganizationReportApplicationDataCommand, UpdateOrganizationReportApplicationDataCommand>();
}
}

View File

@@ -7,5 +7,10 @@ public class AddOrganizationReportRequest
{
public Guid OrganizationId { get; set; }
public string ReportData { get; set; }
public DateTime Date { get; set; }
public string ContentEncryptionKey { get; set; }
public string SummaryData { get; set; }
public string ApplicationData { get; set; }
}

View File

@@ -0,0 +1,11 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class GetOrganizationReportSummaryDataByDateRangeRequest
{
public Guid OrganizationId { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}

View File

@@ -0,0 +1,11 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class UpdateOrganizationReportApplicationDataRequest
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public string ApplicationData { get; set; }
}

View File

@@ -0,0 +1,11 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class UpdateOrganizationReportDataRequest
{
public Guid OrganizationId { get; set; }
public Guid ReportId { get; set; }
public string ReportData { get; set; }
}

View File

@@ -0,0 +1,14 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class UpdateOrganizationReportRequest
{
public Guid ReportId { get; set; }
public Guid OrganizationId { get; set; }
public string ReportData { get; set; }
public string ContentEncryptionKey { get; set; }
public string SummaryData { get; set; } = null;
public string ApplicationData { get; set; }
}

View File

@@ -0,0 +1,11 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class UpdateOrganizationReportSummaryRequest
{
public Guid OrganizationId { get; set; }
public Guid ReportId { get; set; }
public string SummaryData { get; set; }
}

View File

@@ -0,0 +1,96 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class UpdateOrganizationReportApplicationDataCommand : IUpdateOrganizationReportApplicationDataCommand
{
private readonly IOrganizationRepository _organizationRepo;
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<UpdateOrganizationReportApplicationDataCommand> _logger;
public UpdateOrganizationReportApplicationDataCommand(
IOrganizationRepository organizationRepository,
IOrganizationReportRepository organizationReportRepository,
ILogger<UpdateOrganizationReportApplicationDataCommand> logger)
{
_organizationRepo = organizationRepository;
_organizationReportRepo = organizationReportRepository;
_logger = logger;
}
public async Task<OrganizationReport> UpdateOrganizationReportApplicationDataAsync(UpdateOrganizationReportApplicationDataRequest request)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report application data {reportId} for organization {organizationId}",
request.Id, request.OrganizationId);
var (isValid, errorMessage) = await ValidateRequestAsync(request);
if (!isValid)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report application data {reportId} for organization {organizationId}: {errorMessage}",
request.Id, request.OrganizationId, errorMessage);
throw new BadRequestException(errorMessage);
}
var existingReport = await _organizationReportRepo.GetByIdAsync(request.Id);
if (existingReport == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.Id);
throw new NotFoundException("Organization report not found");
}
if (existingReport.OrganizationId != request.OrganizationId)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}",
request.Id, request.OrganizationId);
throw new BadRequestException("Organization report does not belong to the specified organization");
}
var updatedReport = await _organizationReportRepo.UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData);
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report application data {reportId} for organization {organizationId}",
request.Id, request.OrganizationId);
return updatedReport;
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
_logger.LogError(ex, "Error updating organization report application data {reportId} for organization {organizationId}",
request.Id, request.OrganizationId);
throw;
}
}
private async Task<(bool isValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportApplicationDataRequest request)
{
if (request.OrganizationId == Guid.Empty)
{
return (false, "OrganizationId is required");
}
if (request.Id == Guid.Empty)
{
return (false, "Id is required");
}
var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);
if (organization == null)
{
return (false, "Invalid Organization");
}
if (string.IsNullOrWhiteSpace(request.ApplicationData))
{
return (false, "Application Data is required");
}
return (true, string.Empty);
}
}

View File

@@ -0,0 +1,124 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class UpdateOrganizationReportCommand : IUpdateOrganizationReportCommand
{
private readonly IOrganizationRepository _organizationRepo;
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<UpdateOrganizationReportCommand> _logger;
public UpdateOrganizationReportCommand(
IOrganizationRepository organizationRepository,
IOrganizationReportRepository organizationReportRepository,
ILogger<UpdateOrganizationReportCommand> logger)
{
_organizationRepo = organizationRepository;
_organizationReportRepo = organizationReportRepository;
_logger = logger;
}
public async Task<OrganizationReport> UpdateOrganizationReportAsync(UpdateOrganizationReportRequest request)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
var (isValid, errorMessage) = await ValidateRequestAsync(request);
if (!isValid)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report {reportId} for organization {organizationId}: {errorMessage}",
request.ReportId, request.OrganizationId, errorMessage);
throw new BadRequestException(errorMessage);
}
var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId);
if (existingReport == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.ReportId);
throw new NotFoundException("Organization report not found");
}
if (existingReport.OrganizationId != request.OrganizationId)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}",
request.ReportId, request.OrganizationId);
throw new BadRequestException("Organization report does not belong to the specified organization");
}
existingReport.ContentEncryptionKey = request.ContentEncryptionKey;
existingReport.SummaryData = request.SummaryData;
existingReport.ReportData = request.ReportData;
existingReport.ApplicationData = request.ApplicationData;
existingReport.RevisionDate = DateTime.UtcNow;
await _organizationReportRepo.UpsertAsync(existingReport);
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
var response = await _organizationReportRepo.GetByIdAsync(request.ReportId);
if (response == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found after update", request.ReportId);
throw new NotFoundException("Organization report not found after update");
}
return response;
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
_logger.LogError(ex, "Error updating organization report {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
throw;
}
}
private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportRequest request)
{
if (request.OrganizationId == Guid.Empty)
{
return (false, "OrganizationId is required");
}
if (request.ReportId == Guid.Empty)
{
return (false, "ReportId is required");
}
var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);
if (organization == null)
{
return (false, "Invalid Organization");
}
if (string.IsNullOrWhiteSpace(request.ContentEncryptionKey))
{
return (false, "ContentEncryptionKey is required");
}
if (string.IsNullOrWhiteSpace(request.ReportData))
{
return (false, "Report Data is required");
}
if (string.IsNullOrWhiteSpace(request.SummaryData))
{
return (false, "Summary Data is required");
}
if (string.IsNullOrWhiteSpace(request.ApplicationData))
{
return (false, "Application Data is required");
}
return (true, string.Empty);
}
}

View File

@@ -0,0 +1,96 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class UpdateOrganizationReportDataCommand : IUpdateOrganizationReportDataCommand
{
private readonly IOrganizationRepository _organizationRepo;
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<UpdateOrganizationReportDataCommand> _logger;
public UpdateOrganizationReportDataCommand(
IOrganizationRepository organizationRepository,
IOrganizationReportRepository organizationReportRepository,
ILogger<UpdateOrganizationReportDataCommand> logger)
{
_organizationRepo = organizationRepository;
_organizationReportRepo = organizationReportRepository;
_logger = logger;
}
public async Task<OrganizationReport> UpdateOrganizationReportDataAsync(UpdateOrganizationReportDataRequest request)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report data {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
var (isValid, errorMessage) = await ValidateRequestAsync(request);
if (!isValid)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report data {reportId} for organization {organizationId}: {errorMessage}",
request.ReportId, request.OrganizationId, errorMessage);
throw new BadRequestException(errorMessage);
}
var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId);
if (existingReport == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.ReportId);
throw new NotFoundException("Organization report not found");
}
if (existingReport.OrganizationId != request.OrganizationId)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}",
request.ReportId, request.OrganizationId);
throw new BadRequestException("Organization report does not belong to the specified organization");
}
var updatedReport = await _organizationReportRepo.UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData);
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report data {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
return updatedReport;
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
_logger.LogError(ex, "Error updating organization report data {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
throw;
}
}
private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportDataRequest request)
{
if (request.OrganizationId == Guid.Empty)
{
return (false, "OrganizationId is required");
}
if (request.ReportId == Guid.Empty)
{
return (false, "ReportId is required");
}
var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);
if (organization == null)
{
return (false, "Invalid Organization");
}
if (string.IsNullOrWhiteSpace(request.ReportData))
{
return (false, "Report Data is required");
}
return (true, string.Empty);
}
}

View File

@@ -0,0 +1,96 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class UpdateOrganizationReportSummaryCommand : IUpdateOrganizationReportSummaryCommand
{
private readonly IOrganizationRepository _organizationRepo;
private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly ILogger<UpdateOrganizationReportSummaryCommand> _logger;
public UpdateOrganizationReportSummaryCommand(
IOrganizationRepository organizationRepository,
IOrganizationReportRepository organizationReportRepository,
ILogger<UpdateOrganizationReportSummaryCommand> logger)
{
_organizationRepo = organizationRepository;
_organizationReportRepo = organizationReportRepository;
_logger = logger;
}
public async Task<OrganizationReport> UpdateOrganizationReportSummaryAsync(UpdateOrganizationReportSummaryRequest request)
{
try
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report summary {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
var (isValid, errorMessage) = await ValidateRequestAsync(request);
if (!isValid)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report summary {reportId} for organization {organizationId}: {errorMessage}",
request.ReportId, request.OrganizationId, errorMessage);
throw new BadRequestException(errorMessage);
}
var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId);
if (existingReport == null)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.ReportId);
throw new NotFoundException("Organization report not found");
}
if (existingReport.OrganizationId != request.OrganizationId)
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}",
request.ReportId, request.OrganizationId);
throw new BadRequestException("Organization report does not belong to the specified organization");
}
var updatedReport = await _organizationReportRepo.UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData);
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report summary {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
return updatedReport;
}
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
{
_logger.LogError(ex, "Error updating organization report summary {reportId} for organization {organizationId}",
request.ReportId, request.OrganizationId);
throw;
}
}
private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportSummaryRequest request)
{
if (request.OrganizationId == Guid.Empty)
{
return (false, "OrganizationId is required");
}
if (request.ReportId == Guid.Empty)
{
return (false, "ReportId is required");
}
var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);
if (organization == null)
{
return (false, "Invalid Organization");
}
if (string.IsNullOrWhiteSpace(request.SummaryData))
{
return (false, "Summary Data is required");
}
return (true, string.Empty);
}
}

View File

@@ -1,12 +1,25 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Repositories;
namespace Bit.Core.Dirt.Repositories;
public interface IOrganizationReportRepository : IRepository<OrganizationReport, Guid>
{
Task<ICollection<OrganizationReport>> GetByOrganizationIdAsync(Guid organizationId);
// Whole OrganizationReport methods
Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId);
// SummaryData methods
Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetSummaryDataByDateRangeAsync(Guid organizationId, DateTime startDate, DateTime endDate);
Task<OrganizationReportSummaryDataResponse> GetSummaryDataAsync(Guid reportId);
Task<OrganizationReport> UpdateSummaryDataAsync(Guid orgId, Guid reportId, string summaryData);
// ReportData methods
Task<OrganizationReportDataResponse> GetReportDataAsync(Guid reportId);
Task<OrganizationReport> UpdateReportDataAsync(Guid orgId, Guid reportId, string reportData);
// ApplicationData methods
Task<OrganizationReportApplicationDataResponse> GetApplicationDataAsync(Guid reportId);
Task<OrganizationReport> UpdateApplicationDataAsync(Guid orgId, Guid reportId, string applicationData);
}

View File

@@ -3,6 +3,7 @@
using System.Data;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
@@ -23,26 +24,153 @@ public class OrganizationReportRepository : Repository<OrganizationReport, Guid>
{
}
public async Task<ICollection<OrganizationReport>> GetByOrganizationIdAsync(Guid organizationId)
public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId)
{
using (var connection = new SqlConnection(ReadOnlyConnectionString))
{
var results = await connection.QueryAsync<OrganizationReport>(
$"[{Schema}].[OrganizationReport_ReadByOrganizationId]",
var result = await connection.QuerySingleOrDefaultAsync<OrganizationReport>(
$"[{Schema}].[OrganizationReport_GetLatestByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
return results.ToList();
return result;
}
}
public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId)
public async Task<OrganizationReport> UpdateSummaryDataAsync(Guid organizationId, Guid reportId, string summaryData)
{
return await GetByOrganizationIdAsync(organizationId)
.ContinueWith(task =>
using (var connection = new SqlConnection(ConnectionString))
{
var reports = task.Result;
return reports.OrderByDescending(r => r.CreationDate).FirstOrDefault();
});
var parameters = new
{
Id = reportId,
OrganizationId = organizationId,
SummaryData = summaryData,
RevisionDate = DateTime.UtcNow
};
await connection.ExecuteAsync(
$"[{Schema}].[OrganizationReport_UpdateSummaryData]",
parameters,
commandType: CommandType.StoredProcedure);
// Return the updated report
return await connection.QuerySingleOrDefaultAsync<OrganizationReport>(
$"[{Schema}].[OrganizationReport_ReadById]",
new { Id = reportId },
commandType: CommandType.StoredProcedure);
}
}
public async Task<OrganizationReportSummaryDataResponse> GetSummaryDataAsync(Guid reportId)
{
using (var connection = new SqlConnection(ReadOnlyConnectionString))
{
var result = await connection.QuerySingleOrDefaultAsync<OrganizationReportSummaryDataResponse>(
$"[{Schema}].[OrganizationReport_GetSummaryDataById]",
new { Id = reportId },
commandType: CommandType.StoredProcedure);
return result;
}
}
public async Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetSummaryDataByDateRangeAsync(
Guid organizationId,
DateTime startDate, DateTime
endDate)
{
using (var connection = new SqlConnection(ReadOnlyConnectionString))
{
var parameters = new
{
OrganizationId = organizationId,
StartDate = startDate,
EndDate = endDate
};
var results = await connection.QueryAsync<OrganizationReportSummaryDataResponse>(
$"[{Schema}].[OrganizationReport_GetSummariesByDateRange]",
parameters,
commandType: CommandType.StoredProcedure);
return results;
}
}
public async Task<OrganizationReportDataResponse> GetReportDataAsync(Guid reportId)
{
using (var connection = new SqlConnection(ReadOnlyConnectionString))
{
var result = await connection.QuerySingleOrDefaultAsync<OrganizationReportDataResponse>(
$"[{Schema}].[OrganizationReport_GetReportDataById]",
new { Id = reportId },
commandType: CommandType.StoredProcedure);
return result;
}
}
public async Task<OrganizationReport> UpdateReportDataAsync(Guid organizationId, Guid reportId, string reportData)
{
using (var connection = new SqlConnection(ConnectionString))
{
var parameters = new
{
OrganizationId = organizationId,
Id = reportId,
ReportData = reportData,
RevisionDate = DateTime.UtcNow
};
await connection.ExecuteAsync(
$"[{Schema}].[OrganizationReport_UpdateReportData]",
parameters,
commandType: CommandType.StoredProcedure);
// Return the updated report
return await connection.QuerySingleOrDefaultAsync<OrganizationReport>(
$"[{Schema}].[OrganizationReport_ReadById]",
new { Id = reportId },
commandType: CommandType.StoredProcedure);
}
}
public async Task<OrganizationReportApplicationDataResponse> GetApplicationDataAsync(Guid reportId)
{
using (var connection = new SqlConnection(ReadOnlyConnectionString))
{
var result = await connection.QuerySingleOrDefaultAsync<OrganizationReportApplicationDataResponse>(
$"[{Schema}].[OrganizationReport_GetApplicationDataById]",
new { Id = reportId },
commandType: CommandType.StoredProcedure);
return result;
}
}
public async Task<OrganizationReport> UpdateApplicationDataAsync(Guid organizationId, Guid reportId, string applicationData)
{
using (var connection = new SqlConnection(ConnectionString))
{
var parameters = new
{
OrganizationId = organizationId,
Id = reportId,
ApplicationData = applicationData,
RevisionDate = DateTime.UtcNow
};
await connection.ExecuteAsync(
$"[{Schema}].[OrganizationReport_UpdateApplicationData]",
parameters,
commandType: CommandType.StoredProcedure);
// Return the updated report
return await connection.QuerySingleOrDefaultAsync<OrganizationReport>(
$"[{Schema}].[OrganizationReport_ReadById]",
new { Id = reportId },
commandType: CommandType.StoredProcedure);
}
}
}

View File

@@ -3,6 +3,7 @@
using AutoMapper;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using LinqToDB;
@@ -19,18 +20,6 @@ public class OrganizationReportRepository :
IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationReports)
{ }
public async Task<ICollection<OrganizationReport>> GetByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var results = await dbContext.OrganizationReports
.Where(p => p.OrganizationId == organizationId)
.ToListAsync();
return Mapper.Map<ICollection<OrganizationReport>>(results);
}
}
public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
@@ -38,14 +27,161 @@ public class OrganizationReportRepository :
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.OrganizationReports
.Where(p => p.OrganizationId == organizationId)
.OrderByDescending(p => p.Date)
.OrderByDescending(p => p.RevisionDate)
.Take(1)
.FirstOrDefaultAsync();
if (result == null)
return default;
if (result == null) return default;
return Mapper.Map<OrganizationReport>(result);
}
}
public async Task<OrganizationReport> UpdateSummaryDataAsync(Guid organizationId, Guid reportId, string summaryData)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
// Update only SummaryData and RevisionDate
await dbContext.OrganizationReports
.Where(p => p.Id == reportId && p.OrganizationId == organizationId)
.UpdateAsync(p => new Models.OrganizationReport
{
SummaryData = summaryData,
RevisionDate = DateTime.UtcNow
});
// Return the updated report
var updatedReport = await dbContext.OrganizationReports
.Where(p => p.Id == reportId)
.FirstOrDefaultAsync();
return Mapper.Map<OrganizationReport>(updatedReport);
}
}
public async Task<OrganizationReportSummaryDataResponse> GetSummaryDataAsync(Guid reportId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.OrganizationReports
.Where(p => p.Id == reportId)
.Select(p => new OrganizationReportSummaryDataResponse
{
SummaryData = p.SummaryData
})
.FirstOrDefaultAsync();
return result;
}
}
public async Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetSummaryDataByDateRangeAsync(
Guid organizationId,
DateTime startDate,
DateTime endDate)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var results = await dbContext.OrganizationReports
.Where(p => p.OrganizationId == organizationId &&
p.CreationDate >= startDate && p.CreationDate <= endDate)
.Select(p => new OrganizationReportSummaryDataResponse
{
SummaryData = p.SummaryData
})
.ToListAsync();
return results;
}
}
public async Task<OrganizationReportDataResponse> GetReportDataAsync(Guid reportId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.OrganizationReports
.Where(p => p.Id == reportId)
.Select(p => new OrganizationReportDataResponse
{
ReportData = p.ReportData
})
.FirstOrDefaultAsync();
return result;
}
}
public async Task<OrganizationReport> UpdateReportDataAsync(Guid organizationId, Guid reportId, string reportData)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
// Update only ReportData and RevisionDate
await dbContext.OrganizationReports
.Where(p => p.Id == reportId && p.OrganizationId == organizationId)
.UpdateAsync(p => new Models.OrganizationReport
{
ReportData = reportData,
RevisionDate = DateTime.UtcNow
});
// Return the updated report
var updatedReport = await dbContext.OrganizationReports
.Where(p => p.Id == reportId)
.FirstOrDefaultAsync();
return Mapper.Map<OrganizationReport>(updatedReport);
}
}
public async Task<OrganizationReportApplicationDataResponse> GetApplicationDataAsync(Guid reportId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.OrganizationReports
.Where(p => p.Id == reportId)
.Select(p => new OrganizationReportApplicationDataResponse
{
ApplicationData = p.ApplicationData
})
.FirstOrDefaultAsync();
return result;
}
}
public async Task<OrganizationReport> UpdateApplicationDataAsync(Guid organizationId, Guid reportId, string applicationData)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
// Update only ApplicationData and RevisionDate
await dbContext.OrganizationReports
.Where(p => p.Id == reportId && p.OrganizationId == organizationId)
.UpdateAsync(p => new Models.OrganizationReport
{
ApplicationData = applicationData,
RevisionDate = DateTime.UtcNow
});
// Return the updated report
var updatedReport = await dbContext.OrganizationReports
.Where(p => p.Id == reportId)
.FirstOrDefaultAsync();
return Mapper.Map<OrganizationReport>(updatedReport);
}
}
}

View File

@@ -1,26 +1,35 @@
CREATE PROCEDURE [dbo].[OrganizationReport_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@Date DATETIME2(7),
@ReportData NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@ContentEncryptionKey VARCHAR(MAX)
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@ReportData NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@ContentEncryptionKey VARCHAR(MAX),
@SummaryData NVARCHAR(MAX),
@ApplicationData NVARCHAR(MAX),
@RevisionDate DATETIME2(7)
AS
SET NOCOUNT ON;
BEGIN
SET NOCOUNT ON;
INSERT INTO [dbo].[OrganizationReport](
[Id],
[OrganizationId],
[Date],
[ReportData],
[CreationDate],
[ContentEncryptionKey]
)
VALUES (
@Id,
@OrganizationId,
@Date,
@ReportData,
@CreationDate,
@ContentEncryptionKey
INSERT INTO [dbo].[OrganizationReport](
[Id],
[OrganizationId],
[ReportData],
[CreationDate],
[ContentEncryptionKey],
[SummaryData],
[ApplicationData],
[RevisionDate]
)
VALUES (
@Id,
@OrganizationId,
@ReportData,
@CreationDate,
@ContentEncryptionKey,
@SummaryData,
@ApplicationData,
@RevisionDate
);
END

View File

@@ -0,0 +1,12 @@
CREATE PROCEDURE [dbo].[OrganizationReport_GetApplicationDataById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
[ApplicationData]
FROM [dbo].[OrganizationReportView]
WHERE [Id] = @Id
END

View File

@@ -0,0 +1,19 @@
CREATE PROCEDURE [dbo].[OrganizationReport_GetLatestByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT TOP 1
[Id],
[OrganizationId],
[ReportData],
[CreationDate],
[ContentEncryptionKey],
[SummaryData],
[ApplicationData],
[RevisionDate]
FROM [dbo].[OrganizationReportView]
WHERE [OrganizationId] = @OrganizationId
ORDER BY [RevisionDate] DESC
END

View File

@@ -0,0 +1,12 @@
CREATE PROCEDURE [dbo].[OrganizationReport_GetReportDataById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
[ReportData]
FROM [dbo].[OrganizationReportView]
WHERE [Id] = @Id
END

View File

@@ -0,0 +1,17 @@
CREATE PROCEDURE [dbo].[OrganizationReport_GetSummariesByDateRange]
@OrganizationId UNIQUEIDENTIFIER,
@StartDate DATETIME2(7),
@EndDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
SELECT
[SummaryData]
FROM [dbo].[OrganizationReportView]
WHERE [OrganizationId] = @OrganizationId
AND [RevisionDate] >= @StartDate
AND [RevisionDate] <= @EndDate
ORDER BY [RevisionDate] DESC
END

View File

@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationReport_GetSummaryDataById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
[SummaryData]
FROM [dbo].[OrganizationReportView]
WHERE [Id] = @Id
END

View File

@@ -1,9 +0,0 @@
CREATE PROCEDURE [dbo].[OrganizationReport_ReadByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
SET NOCOUNT ON;
SELECT
*
FROM [dbo].[OrganizationReportView]
WHERE [OrganizationId] = @OrganizationId;

View File

@@ -0,0 +1,23 @@
CREATE PROCEDURE [dbo].[OrganizationReport_Update]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@ReportData NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@ContentEncryptionKey VARCHAR(MAX),
@SummaryData NVARCHAR(MAX),
@ApplicationData NVARCHAR(MAX),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON;
UPDATE [dbo].[OrganizationReport]
SET
[OrganizationId] = @OrganizationId,
[ReportData] = @ReportData,
[CreationDate] = @CreationDate,
[ContentEncryptionKey] = @ContentEncryptionKey,
[SummaryData] = @SummaryData,
[ApplicationData] = @ApplicationData,
[RevisionDate] = @RevisionDate
WHERE [Id] = @Id;
END;

View File

@@ -0,0 +1,16 @@
CREATE PROCEDURE [dbo].[OrganizationReport_UpdateApplicationData]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@ApplicationData NVARCHAR(MAX),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON;
UPDATE [dbo].[OrganizationReport]
SET
[ApplicationData] = @ApplicationData,
[RevisionDate] = @RevisionDate
WHERE [Id] = @Id
AND [OrganizationId] = @OrganizationId;
END

View File

@@ -0,0 +1,16 @@
CREATE PROCEDURE [dbo].[OrganizationReport_UpdateReportData]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@ReportData NVARCHAR(MAX),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON;
UPDATE [dbo].[OrganizationReport]
SET
[ReportData] = @ReportData,
[RevisionDate] = @RevisionDate
WHERE [Id] = @Id
AND [OrganizationId] = @OrganizationId;
END

View File

@@ -0,0 +1,16 @@
CREATE PROCEDURE [dbo].[OrganizationReport_UpdateSummaryData]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@SummaryData NVARCHAR(MAX),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON;
UPDATE [dbo].[OrganizationReport]
SET
[SummaryData] = @SummaryData,
[RevisionDate] = @RevisionDate
WHERE [Id] = @Id
AND [OrganizationId] = @OrganizationId;
END

View File

@@ -1,19 +1,24 @@
CREATE TABLE [dbo].[OrganizationReport] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[Date] DATETIME2 (7) NOT NULL,
[ReportData] NVARCHAR(MAX) NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[ContentEncryptionKey] VARCHAR(MAX) NOT NULL,
[SummaryData] NVARCHAR(MAX) NULL,
[ApplicationData] NVARCHAR(MAX) NULL,
[RevisionDate] DATETIME2 (7) NULL,
CONSTRAINT [PK_OrganizationReport] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationReport_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
);
);
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId]
ON [dbo].[OrganizationReport]([OrganizationId] ASC);
ON [dbo].[OrganizationReport] ([OrganizationId] ASC);
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_Date]
ON [dbo].[OrganizationReport]([OrganizationId] ASC, [Date] DESC);
CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_RevisionDate]
ON [dbo].[OrganizationReport]([OrganizationId] ASC, [RevisionDate] DESC);
GO