|
- using System.Security.Claims;
- using Campaign_Tracker.Server.Audit;
- using Campaign_Tracker.Server.Authorization;
- using Campaign_Tracker.Server.Municipalities;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
-
- namespace Campaign_Tracker.Server.Controllers;
-
- /// <summary>
- /// Municipality account profile management (Story 1.10).
- /// Accessible to ClientServices and Admin roles (HasAny check includes Admin bypass).
- /// </summary>
- [ApiController]
- [Authorize(Policy = ApplicationPolicy.ClientServicesAccess)]
- [Route("api/municipalities/profiles")]
- public sealed class MunicipalityProfileController : ControllerBase
- {
- private readonly IMunicipalityProfileRepository _profiles;
- private readonly IAuditService _audit;
- private readonly TimeProvider _timeProvider;
-
- public MunicipalityProfileController(
- IMunicipalityProfileRepository profiles,
- IAuditService audit,
- TimeProvider timeProvider)
- {
- _profiles = profiles;
- _audit = audit;
- _timeProvider = timeProvider;
- }
-
- // ── AC #1, AC #2: create and immediately return the combined view ─────────
-
- [HttpPost]
- public async Task<ActionResult<MunicipalityProfileResponse>> Create(
- [FromBody] CreateMunicipalityProfileRequest request,
- CancellationToken cancellationToken)
- {
- var actor = GetActor();
- var result = await _profiles.CreateAsync(request.JCode, request.DisplayName, actor, cancellationToken);
-
- if (!result.Saved || result.Profile is null)
- return UnprocessableEntity(new MunicipalityProfileProblem(result.Error ?? "Save failed."));
-
- // AC #3: audit the creation
- _audit.Record(new AuditEvent(
- EventType: "MUNICIPALITY_PROFILE_CREATED",
- ActorIdentity: actor,
- Resource: $"municipalities/profiles/{result.Profile.ProfileId}",
- Outcome: $"created JCode={result.Profile.JCode}",
- TraceIdentifier: HttpContext.TraceIdentifier,
- RecordedAt: _timeProvider.GetUtcNow()));
-
- var view = await _profiles.GetByIdAsync(result.Profile.ProfileId, cancellationToken);
- return Ok(MunicipalityProfileResponse.From(view!));
- }
-
- // ── AC #2: list all profiles with resolved legacy data ───────────────────
-
- [HttpGet]
- public async Task<ActionResult<IReadOnlyList<MunicipalityProfileResponse>>> GetAll(
- CancellationToken cancellationToken)
- {
- var views = await _profiles.GetAllAsync(cancellationToken);
- return Ok(views.Select(MunicipalityProfileResponse.From).ToArray());
- }
-
- [HttpGet("{profileId}")]
- public async Task<ActionResult<MunicipalityProfileResponse>> GetById(
- string profileId,
- CancellationToken cancellationToken)
- {
- var view = await _profiles.GetByIdAsync(profileId, cancellationToken);
- return view is null ? NotFound() : Ok(MunicipalityProfileResponse.From(view));
- }
-
- // ── AC #3: update with audit log ─────────────────────────────────────────
-
- [HttpPut("{profileId}")]
- public async Task<ActionResult<MunicipalityProfileResponse>> Update(
- string profileId,
- [FromBody] UpdateMunicipalityProfileRequest request,
- CancellationToken cancellationToken)
- {
- var actor = GetActor();
- var result = await _profiles.UpdateAsync(profileId, request.DisplayName, actor, cancellationToken);
-
- if (!result.Saved || result.Profile is null)
- return UnprocessableEntity(new MunicipalityProfileProblem(result.Error ?? "Update failed."));
-
- _audit.Record(new AuditEvent(
- EventType: "MUNICIPALITY_PROFILE_UPDATED",
- ActorIdentity: actor,
- Resource: $"municipalities/profiles/{profileId}",
- Outcome: "updated display name",
- TraceIdentifier: HttpContext.TraceIdentifier,
- RecordedAt: _timeProvider.GetUtcNow()));
-
- var view = await _profiles.GetByIdAsync(profileId, cancellationToken);
- return Ok(MunicipalityProfileResponse.From(view!));
- }
-
- private string GetActor() =>
- User.Identity?.Name
- ?? User.FindFirstValue(ClaimTypes.NameIdentifier)
- ?? "unknown";
- }
-
- public sealed record CreateMunicipalityProfileRequest(string JCode, string? DisplayName);
-
- public sealed record UpdateMunicipalityProfileRequest(string? DisplayName);
-
- public sealed record MunicipalityProfileResponse(
- string ProfileId,
- string JCode,
- string? DisplayName,
- string UpdatedAt,
- string UpdatedBy,
- string? LegacyName,
- string? LegacyMailingAddress,
- string? LegacyCityStateZip)
- {
- public static MunicipalityProfileResponse From(MunicipalityProfileView view) =>
- new(view.Profile.ProfileId,
- view.Profile.JCode,
- view.Profile.DisplayName,
- view.Profile.UpdatedAt.ToString("O"),
- view.Profile.UpdatedBy,
- view.LegacyName,
- view.LegacyMailingAddress,
- view.LegacyCityStateZip);
- }
-
- public sealed record MunicipalityProfileProblem(string Error);
|