Skip to content

Notification: fix build path, root-route through gateway, add JWT auth#53

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1782448518-notification-auth
Open

Notification: fix build path, root-route through gateway, add JWT auth#53
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1782448518-notification-auth

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Finishes the Notification microservice carve-out: fixes its broken build, makes it reachable & protected through the gateway, and adds an E2E smoke test. Scoped to src/Services/Notification/** — no monolith, gateway, or compose changes. Existing order-confirmation rendering behavior and response shapes are unchanged.

1. Build fix (Shared project path)

Notification.API.csproj referenced the Shared projects two levels up (..\..\Shared\...), which resolves to the non-existent src/Services/Shared. The service lives one level deeper (src/Services/Notification/Notification.API), so the correct path is three levels up:

- ..\..\Shared\Shared.Contracts\Shared.Contracts.csproj
+ ..\..\..\Shared\Shared.Contracts\Shared.Contracts.csproj   (same for Shared.Infrastructure)

The Dockerfile COPY lines were already correct (paths relative to the src build context: Shared/Shared.Contracts/...), so no Dockerfile change was needed.

Verified: cd src && dotnet build Services/Notification/Notification.API/Notification.API.csproj -c ReleaseBuild succeeded.

2. Root-routing through the gateway

The gateway strips /api/notifications before forwarding to notification-service:5005. The controller used [Route("api/[controller]")] (= /api/notification), so external /api/notifications/* never reached the actions. Removed the controller-level route and pinned each action to an absolute service-root route.

External (via gateway) → service path:

External (gateway) Service route Auth
GET /api/notifications GET / [Authorize]
GET /api/notifications/{id} GET /{id:guid} [Authorize]
GET /api/notifications/{id}/preview GET /{id:guid}/preview [Authorize]
POST /api/notifications/events/order-placed POST /events/order-placed (unauthenticated intake)
GET /api/notifications/healthz GET /healthz (public)

The intake response previewUrl was updated from /api/notification/{id}/preview to the service-root /{id}/preview to match.

3. JWT auth (canonical contract from Identity PR #48)

  • Added NuGet Microsoft.AspNetCore.Authentication.JwtBearer (10.*).
  • Added a Jwt section to appsettings.json (Key/Issuer=quickapp-identity/Audience=quickapp; dev-only signing key).
  • AddAuthentication().AddJwtBearer with TokenValidationParameters validating issuer/audience/lifetime/signing-key (HS256 SymmetricSecurityKey over UTF8 of Jwt:Key), MapInboundClaims=false, NameClaimType="name", RoleClaimType="role".
  • app.UseAuthentication(); app.UseAuthorization(); before MapControllers().
  • Read/query endpoints (list, get-by-id, preview) are [Authorize]; the POST /events/order-placed intake (simulated internal broker event) is left unauthenticated.

4. E2E smoke test

Added src/Services/Notification/e2e-smoke.sh (exits non-zero on failure) asserting through the gateway:

  • GET /api/notifications/healthz → 200
  • GET /api/notifications without a token → 401

Validation

Brought up postgres, notification-service, api-gateway via docker compose and ran the smoke test through http://localhost:5000:

  • GET /api/notifications/healthz200
  • GET /api/notifications (no token) → 401
  • GET /api/notifications (valid HS256 token) → 200
  • POST /api/notifications/events/order-placed201
  • GET /api/notifications/{id}/preview (no token) → 401; (valid token) → 200 with the unchanged rendered HTML email

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/f32b8fe62be54776b075130b3644b409
Requested by: @mbatchelor81


Open in Devin Review

@mbatchelor81 mbatchelor81 self-assigned this Jun 26, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +93 to 94
[HttpPost("/events/order-placed")]
public async Task<IActionResult> ReceiveOrderPlacedEvent([FromBody] OrderPlacedEventDto dto)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Event ingestion endpoint is exposed through the gateway without authentication

The ReceiveOrderPlacedEvent POST endpoint at NotificationController.cs:93 does not have [Authorize], while all GET endpoints do. The comment explains this endpoint simulates a RabbitMQ consumer for local testing. However, the YARP gateway's catch-all route (/api/notifications/{**catch-all} at ApiGateway/appsettings.json:33) will forward POST /api/notifications/events/order-placed to this endpoint, making it publicly reachable without any token. Anyone who can reach the gateway can create notifications with arbitrary data. If this is acceptable for the current development phase, consider adding a TODO or documenting the intent to protect or remove this endpoint before production.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional for this phase. The POST /events/order-placed endpoint simulates an internal message-broker (RabbitMQ) event — in production it'd be a broker consumer, not a gateway-exposed route. The task spec explicitly allows leaving intake unauthenticated; the security gate only requires the read/query endpoints (list/get/preview) to be [Authorize] (401 without a token), which they are. Locking the intake down (or removing the gateway-exposed shim once a real broker is wired) is a follow-up, kept out of scope here to avoid regressing the local-testing flow.

Comment on lines 104 to 108
return CreatedAtAction(
nameof(GetPreview),
new { id = notification.Id },
new { notification.Id, PreviewUrl = $"/api/notification/{notification.Id}/preview" });
new { notification.Id, PreviewUrl = $"/{notification.Id}/preview" });
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 CreatedAtAction Location header will not be routable through the gateway

The CreatedAtAction at NotificationController.cs:104-107 generates a Location header based on the GetPreview action's route /{id:guid}/preview. When accessed through the YARP gateway, the backend generates a URL like http://notification-service:5005/{guid}/preview. Without UseForwardedHeaders() middleware and corresponding YARP X-Forwarded-* header configuration, this Location header points to an internal host the client can't reach. The hardcoded PreviewUrl (/{id}/preview) has the same issue — it lacks the /api/notifications gateway prefix. Note: the pre-PR code had a similar issue (it used /api/notification/ singular, not matching the gateway's /api/notifications/ plural). This is an infrastructure concern rather than a code-level bug, but worth addressing when the service is deployed behind the gateway.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — this is a pre-existing gateway-prefix/forwarded-headers concern, not a regression from this PR. The old value was /api/notification/{id}/preview (singular, already not matching the gateway's plural /api/notifications); I only updated it to the new service-root path /{id}/preview to stay consistent with the root-routing change. Making the Location/PreviewUrl fully gateway-routable requires UseForwardedHeaders() + YARP X-Forwarded-* config (an infra change touching the gateway), which is outside this PR's scope of "scope changes to Notification only" and "keep response shapes unchanged." Leaving as a follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant