|
- 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;
-
- [ApiController]
- [Authorize(Policy = ApplicationPolicy.ClientServicesAccess)]
- [Route("api/municipalities/{profileId}/contacts")]
- public sealed class MunicipalityContactsController : ControllerBase
- {
- private readonly IMunicipalityProfileRepository _profiles;
- private readonly IMunicipalityContactRepository _contacts;
- private readonly IAuditService _audit;
- private readonly TimeProvider _timeProvider;
-
- public MunicipalityContactsController(
- IMunicipalityProfileRepository profiles,
- IMunicipalityContactRepository contacts,
- IAuditService audit,
- TimeProvider timeProvider)
- {
- _profiles = profiles;
- _contacts = contacts;
- _audit = audit;
- _timeProvider = timeProvider;
- }
-
- [HttpGet]
- public async Task<ActionResult<IReadOnlyList<MunicipalityContactResponse>>> GetAll(
- string profileId,
- CancellationToken cancellationToken)
- {
- if (!await ProfileExists(profileId, cancellationToken))
- return NotFound();
-
- var contacts = await _contacts.GetByProfileIdAsync(profileId, cancellationToken);
- return Ok(contacts.Select(MunicipalityContactResponse.From).ToArray());
- }
-
- [HttpGet("{contactId}")]
- public async Task<ActionResult<MunicipalityContactResponse>> GetById(
- string profileId,
- string contactId,
- CancellationToken cancellationToken)
- {
- var contact = await _contacts.GetByIdAsync(contactId, cancellationToken);
- return IsContactInProfile(contact, profileId)
- ? Ok(MunicipalityContactResponse.From(contact!))
- : NotFound();
- }
-
- [HttpPost]
- public async Task<ActionResult<MunicipalityContactResponse>> Add(
- string profileId,
- [FromBody] AddMunicipalityContactRequest request,
- CancellationToken cancellationToken)
- {
- if (!await ProfileExists(profileId, cancellationToken))
- return NotFound();
-
- var actor = GetActor();
- var result = await _contacts.AddAsync(
- profileId,
- request.ContactType,
- request.Name,
- request.RoleTitle,
- request.Phone,
- request.Email,
- actor,
- cancellationToken);
-
- if (!result.Saved || result.Contact is null)
- return UnprocessableEntity(new MunicipalityContactProblem(result.Error ?? "Save failed."));
-
- _audit.Record(new AuditEvent(
- EventType: "MUNICIPALITY_CONTACT_ADDED",
- ActorIdentity: actor,
- Resource: $"municipalities/{profileId}/contacts/{result.Contact.ContactId}",
- Outcome: $"added {result.Contact.ContactType} contact",
- TraceIdentifier: HttpContext.TraceIdentifier,
- RecordedAt: _timeProvider.GetUtcNow()));
-
- return CreatedAtAction(nameof(GetById),
- new { profileId, contactId = result.Contact.ContactId },
- MunicipalityContactResponse.From(result.Contact));
- }
-
- [HttpPut("{contactId}")]
- public async Task<ActionResult<MunicipalityContactResponse>> Update(
- string profileId,
- string contactId,
- [FromBody] UpdateMunicipalityContactRequest request,
- CancellationToken cancellationToken)
- {
- var existing = await _contacts.GetByIdAsync(contactId, cancellationToken);
- if (!IsContactInProfile(existing, profileId))
- return NotFound(new MunicipalityContactProblem("Contact not found."));
-
- var actor = GetActor();
- var result = await _contacts.UpdateAsync(
- contactId,
- request.ContactType,
- request.Name,
- request.RoleTitle,
- request.Phone,
- request.Email,
- actor,
- cancellationToken);
-
- if (!result.Saved || result.Contact is null)
- {
- if (result.IsNotFound)
- return NotFound(new MunicipalityContactProblem(result.Error ?? "Contact not found."));
- return UnprocessableEntity(new MunicipalityContactProblem(result.Error ?? "Update failed."));
- }
-
- _audit.Record(new AuditEvent(
- EventType: "MUNICIPALITY_CONTACT_UPDATED",
- ActorIdentity: actor,
- Resource: $"municipalities/{profileId}/contacts/{contactId}",
- Outcome: $"updated {result.Contact.ContactType} contact",
- TraceIdentifier: HttpContext.TraceIdentifier,
- RecordedAt: _timeProvider.GetUtcNow()));
-
- return Ok(MunicipalityContactResponse.From(result.Contact));
- }
-
- [HttpDelete("{contactId}")]
- public async Task<IActionResult> Delete(
- string profileId,
- string contactId,
- CancellationToken cancellationToken)
- {
- var existing = await _contacts.GetByIdAsync(contactId, cancellationToken);
- if (!IsContactInProfile(existing, profileId))
- return NotFound();
-
- var actor = GetActor();
- var result = await _contacts.SoftDeleteAsync(contactId, actor, cancellationToken);
-
- if (!result.Saved)
- return result.IsNotFound ? NotFound() : UnprocessableEntity();
-
- _audit.Record(new AuditEvent(
- EventType: "MUNICIPALITY_CONTACT_DELETED",
- ActorIdentity: actor,
- Resource: $"municipalities/{profileId}/contacts/{contactId}",
- Outcome: "soft-deleted",
- TraceIdentifier: HttpContext.TraceIdentifier,
- RecordedAt: _timeProvider.GetUtcNow()));
-
- return NoContent();
- }
-
- private string GetActor() =>
- User.Identity?.Name
- ?? User.FindFirstValue(ClaimTypes.NameIdentifier)
- ?? "unknown";
-
- private async Task<bool> ProfileExists(
- string profileId,
- CancellationToken cancellationToken) =>
- await _profiles.GetByIdAsync(profileId, cancellationToken) is not null;
-
- private static bool IsContactInProfile(
- MunicipalityContact? contact,
- string profileId) =>
- contact is not null &&
- string.Equals(contact.ProfileId, profileId, StringComparison.OrdinalIgnoreCase);
- }
-
- public sealed record AddMunicipalityContactRequest(
- string ContactType,
- string Name,
- string? RoleTitle,
- string? Phone,
- string? Email);
-
- public sealed record UpdateMunicipalityContactRequest(
- string ContactType,
- string Name,
- string? RoleTitle,
- string? Phone,
- string? Email);
-
- public sealed record MunicipalityContactResponse(
- string ContactId,
- string ProfileId,
- string ContactType,
- string Name,
- string? RoleTitle,
- string? Phone,
- string? Email,
- string CreatedAt,
- string CreatedBy,
- string UpdatedAt,
- string UpdatedBy)
- {
- public static MunicipalityContactResponse From(MunicipalityContact c) =>
- new(c.ContactId, c.ProfileId, c.ContactType, c.Name, c.RoleTitle, c.Phone, c.Email,
- c.CreatedAt.ToString("O"), c.CreatedBy, c.UpdatedAt.ToString("O"), c.UpdatedBy);
- }
-
- public sealed record MunicipalityContactProblem(string Error);
|