manager-service/ManagerService/Controllers/NotificationController.cs
2026-03-17 09:10:56 +01:00

225 lines
8.5 KiB
C#

using Hangfire;
using Manager.Services;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSwag.Annotations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ManagerService.Controllers
{
[Authorize(Policy = ManagerService.Service.Security.Policies.InstanceAdmin)]
[ApiController, Route("api/[controller]")]
[OpenApiTag("Notification", Description = "Push notification management")]
public class NotificationController : ControllerBase
{
private readonly ILogger<NotificationController> _logger;
private readonly MyInfoMateDbContext _db;
private readonly NotificationService _notificationService;
IHexIdGeneratorService idService = new HexIdGeneratorService();
public NotificationController(
ILogger<NotificationController> logger,
MyInfoMateDbContext db,
NotificationService notificationService)
{
_logger = logger;
_db = db;
_notificationService = notificationService;
}
/// <summary>
/// Get all notifications for the current instance
/// </summary>
[ProducesResponseType(typeof(System.Collections.Generic.List<PushNotificationDTO>), 200)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet]
public async Task<ObjectResult> Get()
{
try
{
var instanceId = User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value;
var notifications = await _db.PushNotifications
.Where(n => n.InstanceId == instanceId)
.OrderByDescending(n => n.DateCreation)
.ToListAsync();
return new OkObjectResult(notifications.Select(n => n.ToDTO()));
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Send a push notification immediately to all devices of the instance
/// </summary>
[ProducesResponseType(typeof(PushNotificationDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 500)]
[HttpPost("send")]
public async Task<ObjectResult> Send([FromBody] SendNotificationDTO dto)
{
try
{
if (dto == null)
throw new ArgumentNullException("Notification param is null");
var instanceId = User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value;
var instance = await _db.Instances.FirstOrDefaultAsync(i => i.Id == instanceId);
if (instance == null || !instance.IsPushNotification)
throw new InvalidOperationException("Push notifications are not enabled for this instance");
var notification = new PushNotification
{
Id = idService.GenerateHexId(),
InstanceId = instanceId,
Title = dto.title,
Body = dto.body,
Topic = $"instance_{instanceId}",
Status = PushNotificationStatus.Scheduled,
DateCreation = DateTime.UtcNow
};
_db.PushNotifications.Add(notification);
await _db.SaveChangesAsync();
var jobId = BackgroundJob.Enqueue<NotificationService>(s => s.SendToTopicAsync(notification.Id));
notification.HangfireJobId = jobId;
await _db.SaveChangesAsync();
return new OkObjectResult(notification.ToDTO());
}
catch (ArgumentNullException ex)
{
return new BadRequestObjectResult(ex.Message);
}
catch (InvalidOperationException ex)
{
return new ConflictObjectResult(ex.Message);
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Schedule a push notification for a future date/time
/// </summary>
[ProducesResponseType(typeof(PushNotificationDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 500)]
[HttpPost("schedule")]
public async Task<ObjectResult> Schedule([FromBody] ScheduleNotificationDTO dto)
{
try
{
if (dto == null)
throw new ArgumentNullException("Notification param is null");
if (dto.scheduledAt <= DateTime.UtcNow)
throw new ArgumentException("Scheduled date must be in the future");
var instanceId = User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value;
var instance = await _db.Instances.FirstOrDefaultAsync(i => i.Id == instanceId);
if (instance == null || !instance.IsPushNotification)
throw new InvalidOperationException("Push notifications are not enabled for this instance");
var notification = new PushNotification
{
Id = idService.GenerateHexId(),
InstanceId = instanceId,
Title = dto.title,
Body = dto.body,
Topic = $"instance_{instanceId}",
Status = PushNotificationStatus.Scheduled,
ScheduledAt = dto.scheduledAt.ToUniversalTime(),
DateCreation = DateTime.UtcNow
};
_db.PushNotifications.Add(notification);
await _db.SaveChangesAsync();
var delay = dto.scheduledAt.ToUniversalTime() - DateTime.UtcNow;
var jobId = BackgroundJob.Schedule<NotificationService>(s => s.SendToTopicAsync(notification.Id), delay);
notification.HangfireJobId = jobId;
await _db.SaveChangesAsync();
return new OkObjectResult(notification.ToDTO());
}
catch (ArgumentNullException ex)
{
return new BadRequestObjectResult(ex.Message);
}
catch (ArgumentException ex)
{
return new BadRequestObjectResult(ex.Message);
}
catch (InvalidOperationException ex)
{
return new ConflictObjectResult(ex.Message);
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Cancel a scheduled notification
/// </summary>
[ProducesResponseType(typeof(string), 202)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpDelete("{id}")]
public async Task<ObjectResult> Cancel(string id)
{
try
{
var instanceId = User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value;
var notification = await _db.PushNotifications
.FirstOrDefaultAsync(n => n.Id == id && n.InstanceId == instanceId);
if (notification == null)
throw new KeyNotFoundException("Notification not found");
if (notification.Status != PushNotificationStatus.Scheduled)
throw new InvalidOperationException("Only scheduled notifications can be cancelled");
if (notification.HangfireJobId != null)
BackgroundJob.Delete(notification.HangfireJobId);
_db.PushNotifications.Remove(notification);
await _db.SaveChangesAsync();
return new ObjectResult("Notification cancelled") { StatusCode = 202 };
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message);
}
catch (InvalidOperationException ex)
{
return new ConflictObjectResult(ex.Message);
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
}
}