You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

162 lines
6.3KB

  1. using System.Security.Claims;
  2. using Campaign_Tracker.Server.Audit;
  3. using Campaign_Tracker.Server.Authorization;
  4. using Campaign_Tracker.Server.LegacyData;
  5. using Campaign_Tracker.Server.Municipalities;
  6. using Microsoft.AspNetCore.Authorization;
  7. using Microsoft.AspNetCore.Mvc;
  8. namespace Campaign_Tracker.Server.Controllers;
  9. /// <summary>
  10. /// Municipality account profile management (Story 1.10).
  11. /// Accessible to ClientServices and Admin roles (HasAny check includes Admin bypass).
  12. /// </summary>
  13. [ApiController]
  14. [Authorize(Policy = ApplicationPolicy.ClientServicesAccess)]
  15. [Route("api/municipalities/profiles")]
  16. public sealed class MunicipalityProfileController : ControllerBase
  17. {
  18. private readonly IMunicipalityProfileRepository _profiles;
  19. private readonly ILegacyDataAccess _legacyData;
  20. private readonly IAuditService _audit;
  21. private readonly TimeProvider _timeProvider;
  22. public MunicipalityProfileController(
  23. IMunicipalityProfileRepository profiles,
  24. ILegacyDataAccess legacyData,
  25. IAuditService audit,
  26. TimeProvider timeProvider)
  27. {
  28. _profiles = profiles;
  29. _legacyData = legacyData;
  30. _audit = audit;
  31. _timeProvider = timeProvider;
  32. }
  33. // ── Available legacy jurisdictions (JCode picker source) ─────────────────
  34. [HttpGet("/api/municipalities/jurisdictions")]
  35. public async Task<ActionResult<IReadOnlyList<LegacyJurisdictionResponse>>> GetJurisdictions(
  36. CancellationToken cancellationToken)
  37. {
  38. var jurisdictions = await _legacyData.GetAllJurisdictionsAsync(cancellationToken);
  39. return Ok(jurisdictions
  40. .Select(j => new LegacyJurisdictionResponse(j.JCode, j.Name))
  41. .ToArray());
  42. }
  43. // ── AC #1, AC #2: create and immediately return the combined view ─────────
  44. [HttpPost]
  45. public async Task<ActionResult<MunicipalityProfileResponse>> Create(
  46. [FromBody] CreateMunicipalityProfileRequest request,
  47. CancellationToken cancellationToken)
  48. {
  49. var actor = GetActor();
  50. var result = await _profiles.CreateAsync(request.JCode, request.DisplayName, actor, cancellationToken);
  51. if (!result.Saved || result.Profile is null)
  52. return UnprocessableEntity(new MunicipalityProfileProblem(result.Error ?? "Save failed."));
  53. // AC #3: audit the creation
  54. _audit.Record(new AuditEvent(
  55. EventType: "MUNICIPALITY_PROFILE_CREATED",
  56. ActorIdentity: actor,
  57. Resource: $"municipalities/profiles/{result.Profile.ProfileId}",
  58. Outcome: $"created JCode={result.Profile.JCode}",
  59. TraceIdentifier: HttpContext.TraceIdentifier,
  60. RecordedAt: _timeProvider.GetUtcNow()));
  61. var view = await _profiles.GetByIdAsync(result.Profile.ProfileId, cancellationToken);
  62. if (view is null)
  63. return StatusCode(500, new MunicipalityProfileProblem("Profile was saved but could not be retrieved."));
  64. return Ok(MunicipalityProfileResponse.From(view));
  65. }
  66. // ── AC #2: list all profiles with resolved legacy data ───────────────────
  67. [HttpGet]
  68. public async Task<ActionResult<IReadOnlyList<MunicipalityProfileResponse>>> GetAll(
  69. CancellationToken cancellationToken)
  70. {
  71. var views = await _profiles.GetAllAsync(cancellationToken);
  72. return Ok(views.Select(MunicipalityProfileResponse.From).ToArray());
  73. }
  74. [HttpGet("{profileId}")]
  75. public async Task<ActionResult<MunicipalityProfileResponse>> GetById(
  76. string profileId,
  77. CancellationToken cancellationToken)
  78. {
  79. var view = await _profiles.GetByIdAsync(profileId, cancellationToken);
  80. return view is null ? NotFound() : Ok(MunicipalityProfileResponse.From(view));
  81. }
  82. // ── AC #3: update with audit log ─────────────────────────────────────────
  83. [HttpPut("{profileId}")]
  84. public async Task<ActionResult<MunicipalityProfileResponse>> Update(
  85. string profileId,
  86. [FromBody] UpdateMunicipalityProfileRequest request,
  87. CancellationToken cancellationToken)
  88. {
  89. var actor = GetActor();
  90. var result = await _profiles.UpdateAsync(profileId, request.DisplayName, actor, cancellationToken);
  91. if (!result.Saved || result.Profile is null)
  92. {
  93. if (result.IsNotFound)
  94. return NotFound(new MunicipalityProfileProblem(result.Error ?? "Profile not found."));
  95. return UnprocessableEntity(new MunicipalityProfileProblem(result.Error ?? "Update failed."));
  96. }
  97. _audit.Record(new AuditEvent(
  98. EventType: "MUNICIPALITY_PROFILE_UPDATED",
  99. ActorIdentity: actor,
  100. Resource: $"municipalities/profiles/{profileId}",
  101. Outcome: "updated display name",
  102. TraceIdentifier: HttpContext.TraceIdentifier,
  103. RecordedAt: _timeProvider.GetUtcNow()));
  104. var view = await _profiles.GetByIdAsync(profileId, cancellationToken);
  105. if (view is null)
  106. return StatusCode(500, new MunicipalityProfileProblem("Profile was updated but could not be retrieved."));
  107. return Ok(MunicipalityProfileResponse.From(view));
  108. }
  109. private string GetActor() =>
  110. User.Identity?.Name
  111. ?? User.FindFirstValue(ClaimTypes.NameIdentifier)
  112. ?? "unknown";
  113. }
  114. public sealed record CreateMunicipalityProfileRequest(string JCode, string? DisplayName);
  115. public sealed record UpdateMunicipalityProfileRequest(string? DisplayName);
  116. public sealed record MunicipalityProfileResponse(
  117. string ProfileId,
  118. string JCode,
  119. string? DisplayName,
  120. string UpdatedAt,
  121. string UpdatedBy,
  122. string? LegacyName,
  123. string? LegacyMailingAddress,
  124. string? LegacyCityStateZip)
  125. {
  126. public static MunicipalityProfileResponse From(MunicipalityProfileView view) =>
  127. new(view.Profile.ProfileId,
  128. view.Profile.JCode,
  129. view.Profile.DisplayName,
  130. view.Profile.UpdatedAt.ToString("O"),
  131. view.Profile.UpdatedBy,
  132. view.LegacyName,
  133. view.LegacyMailingAddress,
  134. view.LegacyCityStateZip);
  135. }
  136. public sealed record MunicipalityProfileProblem(string Error);
  137. public sealed record LegacyJurisdictionResponse(string JCode, string? Name);

Powered by TurnKey Linux.