diff --git a/src/Services/Notification/Notification.API/Controllers/NotificationController.cs b/src/Services/Notification/Notification.API/Controllers/NotificationController.cs index 15d2ebc..54ec210 100644 --- a/src/Services/Notification/Notification.API/Controllers/NotificationController.cs +++ b/src/Services/Notification/Notification.API/Controllers/NotificationController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Notification.API.Services; using Notification.Domain.Interfaces; @@ -6,7 +7,6 @@ namespace Notification.API.Controllers; [ApiController] -[Route("api/[controller]")] public class NotificationController : ControllerBase { private readonly INotificationRepository _repository; @@ -23,7 +23,8 @@ public NotificationController( _logger = logger; } - [HttpGet] + [Authorize] + [HttpGet("/")] public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 20) { var notifications = await _repository.GetAllAsync(page, pageSize); @@ -41,7 +42,8 @@ public async Task GetAll([FromQuery] int page = 1, [FromQuery] in })); } - [HttpGet("{id:guid}")] + [Authorize] + [HttpGet("/{id:guid}")] public async Task GetById(Guid id) { var notification = await _repository.GetByIdAsync(id); @@ -68,7 +70,8 @@ public async Task GetById(Guid id) /// Returns the rendered HTML email preview for a notification. /// Use this endpoint to visually inspect notification output. /// - [HttpGet("{id:guid}/preview")] + [Authorize] + [HttpGet("/{id:guid}/preview")] [Produces("text/html")] public async Task GetPreview(Guid id) { @@ -87,7 +90,7 @@ public async Task GetPreview(Guid id) /// In production this would be a RabbitMQ consumer; this endpoint /// enables local testing without a message broker. /// - [HttpPost("events/order-placed")] + [HttpPost("/events/order-placed")] public async Task ReceiveOrderPlacedEvent([FromBody] OrderPlacedEventDto dto) { var orderEvent = new OrderPlacedEvent( @@ -101,7 +104,7 @@ public async Task ReceiveOrderPlacedEvent([FromBody] OrderPlacedE return CreatedAtAction( nameof(GetPreview), new { id = notification.Id }, - new { notification.Id, PreviewUrl = $"/api/notification/{notification.Id}/preview" }); + new { notification.Id, PreviewUrl = $"/{notification.Id}/preview" }); } } diff --git a/src/Services/Notification/Notification.API/Notification.API.csproj b/src/Services/Notification/Notification.API/Notification.API.csproj index 25b5fb0..13ac5b1 100644 --- a/src/Services/Notification/Notification.API/Notification.API.csproj +++ b/src/Services/Notification/Notification.API/Notification.API.csproj @@ -7,10 +7,11 @@ - - + + + diff --git a/src/Services/Notification/Notification.API/Program.cs b/src/Services/Notification/Notification.API/Program.cs index 6219c9c..e4acb8c 100644 --- a/src/Services/Notification/Notification.API/Program.cs +++ b/src/Services/Notification/Notification.API/Program.cs @@ -2,7 +2,10 @@ using Notification.Domain.Interfaces; using Notification.Infrastructure.Data; using Notification.Infrastructure.Repositories; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using System.Text; var builder = WebApplication.CreateBuilder(args); @@ -11,6 +14,28 @@ builder.Services.AddSwaggerGen(); builder.Services.AddHealthChecks(); +var jwtSection = builder.Configuration.GetSection("Jwt"); +builder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.MapInboundClaims = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSection["Issuer"], + ValidAudience = jwtSection["Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtSection["Key"]!)), + NameClaimType = "name", + RoleClaimType = "role" + }; + }); +builder.Services.AddAuthorization(); + builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); @@ -32,6 +57,9 @@ app.UseSwaggerUI(); } +app.UseAuthentication(); +app.UseAuthorization(); + app.MapControllers(); app.MapHealthChecks("/healthz"); diff --git a/src/Services/Notification/Notification.API/appsettings.json b/src/Services/Notification/Notification.API/appsettings.json index 335c5c3..0cb411b 100644 --- a/src/Services/Notification/Notification.API/appsettings.json +++ b/src/Services/Notification/Notification.API/appsettings.json @@ -7,5 +7,10 @@ }, "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=notificationdb;Username=postgres;Password=postgres" + }, + "Jwt": { + "Key": "quickapp-dev-only-signing-key-change-me-please-32+chars", + "Issuer": "quickapp-identity", + "Audience": "quickapp" } } diff --git a/src/Services/Notification/e2e-smoke.sh b/src/Services/Notification/e2e-smoke.sh new file mode 100755 index 0000000..520e741 --- /dev/null +++ b/src/Services/Notification/e2e-smoke.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# E2E smoke test for the Notification service through the API gateway. +# +# Verifies (through http://localhost:5000, the gateway, which strips the +# /api/notifications prefix and forwards to notification-service:5005): +# 1. GET /api/notifications/healthz -> 200 (service reachable & healthy) +# 2. GET /api/notifications (no JWT) -> 401 (read endpoints are protected) +# +# Exits non-zero on any failure. +# +# Usage: +# src/Services/Notification/e2e-smoke.sh # uses default GATEWAY_URL +# GATEWAY_URL=http://localhost:5000 ./e2e-smoke.sh +# +set -euo pipefail + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:5000}" +fail=0 + +check() { + local desc="$1" expected="$2" url="$3" + local actual + actual="$(curl -s -o /dev/null -w '%{http_code}' "$url" || echo "000")" + if [[ "$actual" == "$expected" ]]; then + echo "PASS: $desc ($url -> $actual)" + else + echo "FAIL: $desc ($url -> expected $expected, got $actual)" + fail=1 + fi +} + +echo "== Notification E2E smoke (gateway: $GATEWAY_URL) ==" +check "health endpoint returns 200" 200 "$GATEWAY_URL/api/notifications/healthz" +check "list endpoint without token returns 401" 401 "$GATEWAY_URL/api/notifications" + +if [[ "$fail" -ne 0 ]]; then + echo "SMOKE TEST FAILED" + exit 1 +fi + +echo "SMOKE TEST PASSED"