Files
HIPCTF2/docs/guides/notifications.md
T

85 lines
5.7 KiB
Markdown

---
type: guide
title: Notifications
description: SPA-wide notification store + the global HTTP error interceptor that translates backend error codes and statuses into friendly user-facing messages.
tags: [guide, notifications, error, interceptor, toast, ux]
timestamp: 2026-07-23T00:10:00Z
---
# Components
| Symbol | Path | Responsibility |
|-------------------------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| `NotificationService` | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. |
| `errorNotificationInterceptor` | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Functional HTTP interceptor that catches `HttpErrorResponse`, looks up the error code, and pushes a friendly message into `NotificationService`. |
| `NotificationService.messages` | signal (consumed by future toast UI) | Currently the messages signal is appended in-memory only; the list itself doubles as the integration point for a future toast/banner component. Each push also mirrors the message to the browser console (`console.error` for `error`, `console.info` for `info`). |
# Wiring
`frontend/src/main.ts` registers the interceptor **after** `authInterceptor` so
the auth header is already attached when the error interceptor sees
the request:
```ts
provideHttpClient(
withInterceptors([csrfInterceptor, authInterceptor, errorNotificationInterceptor]),
)
```
The interceptor:
1. Catches every `HttpErrorResponse` thrown from any HTTP call.
2. Maps the response to a friendly message using `friendlyMessage(status, err.error)`.
3. Suppresses notifications for endpoints that have their own
user-facing error presentation (see *Suppression rules* below).
4. Pushes the message via `NotificationService.error(...)`.
5. Re-throws the original error so the caller still receives the
`ApiErrorEnvelope` (e.g. `ChallengesApiService.submit` reads
`envelope.code` to render the modal's inline error).
# Friendly message mapping
`friendlyMessage(status, body)` resolves in this order:
| Source | Message |
|-----------------------------------------------------|------------------------------------------------------------------------------|
| `status === 0` (network / CORS / aborted) | `Network error. Please try again.` |
| `body.code` matches `FRIENDLY_MESSAGES` | The mapped friendly text (see table below). |
| `status === 401` or `403` | `Your session is no longer valid.` (unless suppressed). |
| `status === 404` | `Not found.` |
| `status >= 500` | `Server error. Please try again later.` |
| `body.message` (non-empty) | Falls back to that message. |
| otherwise | `Request failed (<status>).` |
`FRIENDLY_MESSAGES` lookup table:
| Error code | Friendly text |
|-----------------------|------------------------------------------------------------|
| `EVENT_NOT_RUNNING` | `Submissions are only accepted while the event is running.` |
| `FLAG_INCORRECT` | `Incorrect flag.` |
| `UNAUTHORIZED` | `Your session is no longer valid.` |
| `FORBIDDEN` | `You are not allowed to perform this action.` |
| `NOT_FOUND` | `Not found.` |
| `VALIDATION_FAILED` | `Invalid input.` |
# Suppression rules
Some errors are expected and have their own UI. To avoid double-toasting:
| URL pattern | Status | Why |
|---------------------------------------|--------|---------------------------------------------------------------------------------------------------|
| `/api/v1/auth/login` `/csrf` `/register` | `401` | A failed login or first-fetch of the CSRF cookie produces a `401`; the form already renders the inline error. |
| `/api/v1/challenges/status` | `401` | The challenges page uses the snapshot as a bootstrap hint only — the SSE stream (`/api/v1/events`) is the source of truth and will deliver the next frame. |
In both cases the interceptor still re-throws the error so callers can
react locally; only the notification push is skipped.
# See also
- [Challenges Board](/guides/challenges-board.md) (uses
`errorNotificationInterceptor` for non-flag submission failures)
- [Authenticated Shell](/guides/authenticated-shell.md) (cross-tab
invalidation propagates from a 401 SSE response via
`NotificationService` is **not** triggered — the shell uses its own
peer-invalidation channel)
- [REST API Overview](/api/rest-overview.md) (error envelope)