diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36ad580b9..eaeca5faa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
### Added
+* [Yavsc.Api] Nouveau point d'entrée `GET /api/v1/notifications/me`
+ pour exposer les notifications publiques et ciblées à PostIt sur la
+ Home page ; les notifications d'administration restent filtrées.
+
### Changed
### Fixed
diff --git a/src/Yavsc.Abstract/Constants.cs b/src/Yavsc.Abstract/Constants.cs
index 0d06582bf..5883fea43 100644
--- a/src/Yavsc.Abstract/Constants.cs
+++ b/src/Yavsc.Abstract/Constants.cs
@@ -33,6 +33,8 @@ namespace Yavsc
public const string ResourceUsagePath = "resource-usage";
public const string ResourceUsageCurrentUserRoute = "me";
public const string ResourceUsageAdminRoute = "admin";
+ public const string NotificationsPath = "notifications";
+ public const string NotificationsCurrentUserRoute = "me";
public const string ResourceUsageDefaultCurrency = "EUR";
public const string ResourceUsageCurrentUserDisplayName = "current-user";
public const string ResourceUsageAdminDisplayName = "administrator";
diff --git a/src/Yavsc.Api.Client/NotificationsApiClient.cs b/src/Yavsc.Api.Client/NotificationsApiClient.cs
new file mode 100644
index 000000000..3e93deb25
--- /dev/null
+++ b/src/Yavsc.Api.Client/NotificationsApiClient.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Yavsc.Abstract.Models.Messaging;
+
+namespace Yavsc.Api.Client;
+
+///
+/// Thin HTTP client for the notifications exposed to PostIt on the home page.
+///
+public sealed class NotificationsApiClient
+{
+ private const string PathPrefix = Constants.NotificationsPath;
+
+ private readonly IYavscApiClient _api;
+ private readonly Func _businessBaseAddress;
+
+ public NotificationsApiClient(IYavscApiClient api, string businessBaseAddress)
+ : this(api, () => businessBaseAddress)
+ {
+ }
+
+ public NotificationsApiClient(IYavscApiClient api, Func businessBaseAddress)
+ {
+ _api = api ?? throw new ArgumentNullException(nameof(api));
+ _businessBaseAddress = businessBaseAddress ?? throw new ArgumentNullException(nameof(businessBaseAddress));
+ _ = ResolveBusinessBaseAddress();
+ }
+
+ public Task> GetCurrentAsync(CancellationToken ct = default)
+ {
+ return _api.CallAsync>(
+ HttpMethod.Get,
+ Absolute($"{PathPrefix}/{Constants.NotificationsCurrentUserRoute}"),
+ ct: ct);
+ }
+
+ public Task> GetAllAsync(CancellationToken ct = default)
+ {
+ return _api.CallAsync>(
+ HttpMethod.Get,
+ Absolute(PathPrefix),
+ ct: ct);
+ }
+
+ private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString();
+
+ private Uri ResolveBusinessBaseAddress()
+ {
+ var raw = _businessBaseAddress();
+ if (string.IsNullOrWhiteSpace(raw))
+ throw new InvalidOperationException("Business base address is required.");
+
+ return new Uri(raw, UriKind.Absolute);
+ }
+}
diff --git a/src/Yavsc.Api/Controllers/Administration/NotificationsApiController.cs b/src/Yavsc.Api/Controllers/Administration/NotificationsApiController.cs
new file mode 100644
index 000000000..7053a5acc
--- /dev/null
+++ b/src/Yavsc.Api/Controllers/Administration/NotificationsApiController.cs
@@ -0,0 +1,50 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Yavsc.Abstract.Models.Messaging;
+using Yavsc.Models;
+using Yavsc.Server.Helpers;
+
+namespace Yavsc.ApiControllers.Administration;
+
+[Produces("application/json")]
+[Route(Constants.APIPrefix + "/" + Constants.NotificationsPath)]
+public sealed class NotificationsApiController : Controller
+{
+ private readonly ApplicationDbContext _dbContext;
+
+ public NotificationsApiController(ApplicationDbContext dbContext)
+ {
+ _dbContext = dbContext;
+ }
+
+ [HttpGet]
+ [HttpGet(Constants.NotificationsCurrentUserRoute)]
+ [AllowAnonymous]
+ public async Task>> GetCurrentUserNotifications()
+ {
+ var currentUserId = User.GetUserId();
+ var isAdmin = User.IsInRole(Constants.AdminGroupName);
+
+ var dismissedIds = string.IsNullOrWhiteSpace(currentUserId)
+ ? new HashSet()
+ : await _dbContext.DismissClicked
+ .AsNoTracking()
+ .Where(d => d.UserId == currentUserId)
+ .Select(d => d.NotificationId)
+ .ToHashSetAsync();
+
+ var notifications = await _dbContext.Notification
+ .AsNoTracking()
+ .Where(n => !dismissedIds.Contains(n.Id))
+ .Where(n =>
+ string.IsNullOrWhiteSpace(n.Target)
+ || (n.Target != null && n.Target.StartsWith("pub/"))
+ || (!string.IsNullOrWhiteSpace(currentUserId) && n.Target == $"user/{currentUserId}")
+ || (isAdmin && n.Target == "administration"))
+ .OrderByDescending(n => n.Id)
+ .ToListAsync();
+
+ return Ok(notifications);
+ }
+}
diff --git a/src/test/Yavsc.Api.Tests/ResourceUsageApiControllerTests.cs b/src/test/Yavsc.Api.Tests/ResourceUsageApiControllerTests.cs
index 33b3f392a..9bb278551 100644
--- a/src/test/Yavsc.Api.Tests/ResourceUsageApiControllerTests.cs
+++ b/src/test/Yavsc.Api.Tests/ResourceUsageApiControllerTests.cs
@@ -4,6 +4,7 @@ using System.Net.Http.Json;
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using Yavsc.Abstract.Models.Messaging;
using Yavsc.Abstract.Resources;
using Yavsc.Api.Test.Fixtures;
using Yavsc.Server.Helpers;
@@ -115,6 +116,49 @@ public sealed class ResourceUsageApiControllerTests : IClassFixture 0m);
}
+ [Fact]
+ public async Task GetCurrentUserNotifications_returns_public_and_targeted_notifications()
+ {
+ using var scope = _fixture.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ db.Notification.RemoveRange(db.Notification);
+ await db.Notification.AddRangeAsync(
+ new Notification
+ {
+ title = "Public",
+ body = "Seen by everyone",
+ click_action = "/public",
+ Target = null,
+ },
+ new Notification
+ {
+ title = "User",
+ body = "Private to current user",
+ click_action = "/profile",
+ Target = "user/telemetry-user",
+ },
+ new Notification
+ {
+ title = "Admin",
+ body = "Restricted to administrators",
+ click_action = "/admin",
+ Target = "administration",
+ });
+ await db.SaveChangesAsync();
+
+ using var http = NewClient(_fixture, "telemetry-user");
+
+ var response = await http.GetAsync("/api/v1/notifications/me", TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var notifications = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken);
+ Assert.NotNull(notifications);
+ Assert.Contains(notifications!, n => n.title == "Public");
+ Assert.Contains(notifications!, n => n.title == "User");
+ Assert.DoesNotContain(notifications!, n => n.title == "Admin");
+ }
+
[Fact]
public async Task GetAdminOverview_returns_latest_usage_snapshot_for_each_user()
{