Security and quality fixes #28

Merged
eliott merged 14 commits from feature/security-and-quality-fixes into develop 2026-05-12 22:09:10 +02:00
Owner

Critical and Important fixes from a multi-agent code review (2 of 3 repos).

Highlights

  • SSR crash fix on /mon-compte/tracker (window before isPlatformBrowser)
  • Sanitize Quill innerHTML on info/activity pages (DomSanitizer)
  • Refresh interceptor: error queued requests on refresh failure (no more hang)
  • verify-email: subscribe to refresh() so token rotation actually fires
  • withCredentials globally; helmet CSP in src/server.ts; dead server-cookie removed
  • Guards return UrlTree (proper redirect); returnUrl after login; JWT exp check
  • Subscription leaks fixed via switchMap + takeUntilDestroyed
  • provideClientHydration with event replay and POST exclusion
  • OnPush change detection on every component
  • Defensive parseJwt (booleans / number / array, accepts old + new shapes)
  • PASSWORD_PATTERN aligned with API rule
  • Per-route Title/Meta; lang='fr'
  • Legacy err.error.exceptionMessage migrated to err.error?.data?.message (17 admin/tracker files)

Verification

  • pnpm build clean
  • SSR smoke-test: 8 routes return 200 incl. /mon-compte/tracker (previously crashing)
  • pnpm tsc --noEmit clean
  • 14 commits, +627 / -322 across 68 files
Critical and Important fixes from a multi-agent code review (2 of 3 repos). ## Highlights - SSR crash fix on /mon-compte/tracker (window before isPlatformBrowser) - Sanitize Quill innerHTML on info/activity pages (DomSanitizer) - Refresh interceptor: error queued requests on refresh failure (no more hang) - verify-email: subscribe to refresh() so token rotation actually fires - withCredentials globally; helmet CSP in src/server.ts; dead server-cookie removed - Guards return UrlTree (proper redirect); returnUrl after login; JWT exp check - Subscription leaks fixed via switchMap + takeUntilDestroyed - provideClientHydration with event replay and POST exclusion - OnPush change detection on every component - Defensive parseJwt (booleans / number / array, accepts old + new shapes) - PASSWORD_PATTERN aligned with API rule - Per-route Title/Meta; lang='fr' - Legacy err.error.exceptionMessage migrated to err.error?.data?.message (17 admin/tracker files) ## Verification - pnpm build clean - SSR smoke-test: 8 routes return 200 incl. /mon-compte/tracker (previously crashing) - pnpm tsc --noEmit clean - 14 commits, +627 / -322 across 68 files
Swap operands in the platform check so `window.innerWidth` is only read
after `isPlatformBrowser(this.platformId)` returns true. Previously the
short-circuit evaluation read `window` on the server and crashed the
SSR prerender for `/mon-compte/tracker`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admin-authored content was bound to `[innerHTML]` directly, which would
inject anything Quill stored — a stored-XSS vector if an admin account
were compromised. Each page now sanitises the content through
`DomSanitizer.sanitize(SecurityContext.HTML, ...)` exposed as a
`sanitizedContent` signal. The Angular sanitiser keeps the safe HTML
subset that Quill emits and is platform-safe (works under SSR).

Also wires up `Title`, switches the params subscription to a
`switchMap` + `takeUntilDestroyed` chain, and opts both pages in to
`ChangeDetectionStrategy.OnPush`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous catch branch reset `isRefreshing` and called
`refreshTokenSubject.next(null)`. Queued requests filter out `null`,
so they never emitted and hung forever. Now we surface the failure
via `.error()` on the existing subject, replace it with a fresh one
for the next cycle, and propagate via `throwError`.

Also:
- Detect refresh success via the envelope's `data` shape (a non-empty
  string token) rather than the legacy `statusCode` field, since the
  new unified envelope returns `ApiResponse<{message}>` on failure.
- Add `withCredentials: true` globally via the authorization
  interceptor so the refresh-token cookie is sent on every request,
  and drop the per-call `withCredentials` flags in `AuthService`.
- Add `AuthService.deleteToken()` so the interceptor can clear the
  local session cleanly.
- `AuthService.logout()` now clears local state even when the network
  call fails before bubbling the error.
- Inject `Title` plumbing prep moved to other commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`authService.refresh()` returns a cold Observable that never fires
unless subscribed. Previously the verify-email component called it
but immediately navigated, so the JWT was never refreshed and the
user landed on /mon-compte/infos with stale `emailActive` claims.

Wrap navigation inside the refresh subscription's next/error handlers
so the token is rotated first, then route the user. Also switch the
nested params subscription to a switchMap chain with
`takeUntilDestroyed` to avoid the subscription leak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lock down third-party origins with a strict Content Security Policy
via helmet. The API origin is read from the `API_ORIGIN` env var at
startup so prod/staging can override it. Inline styles are still
allowed because some PrimeNG components emit them.

Also removes the `res.cookie('server-cookie', randomUUID())` line
that was leftover scaffolding (no flags, no expiry, no use).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- All four guards now return a `UrlTree` redirect instead of silently
  returning `false`. authGuard redirects to `/connexion?returnUrl=...`,
  anonymousGuard to `/`, roleGuard to `/`, accountActiveGuard to
  `/mon-compte/infos` so the user can resend the verification email.
- roleGuard now throws in dev mode when the route forgets to declare
  `data.role`, so misconfiguration is caught immediately.
- Login reads the `returnUrl` query param after a successful login,
  rejecting non-relative paths to prevent open-redirects.
- `parseJwt` now treats expired tokens as unauthenticated (returns
  `null`) so the constructor wiring picks that up and the user is
  effectively logged out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace nested `route.params.subscribe(...)` patterns and the
unbounded `router.events.subscribe(...)` in navbar with proper
switchMap chains tied to `takeUntilDestroyed(this.destroyRef)` so
the streams are torn down when the components destroy.

Also moves MessageService/ConfirmationService out of MainLayout's
providers; they're now declared globally so non-MainLayout routes
(login, signup) share the same instance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enable client hydration with `withEventReplay()` so events captured
during SSR-to-CSR handoff are replayed once the app boots, and
`withHttpTransferCacheOptions({includePostRequests: false})` so the
SSR transfer cache only deduplicates safe GET fetches — POSTs like
refresh/login/logout must always go through the client.

Also hoists `ConfirmationService` to the root injector so routes
outside MainLayout (login, signup, verify-email) can use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the legacy MEDIUM_PASSWORD_REGEX rule with the new API
contract: at least 12 chars, including one digit, one uppercase,
one lowercase and one non-alphanumeric symbol. Exposes a shared
`PASSWORD_RULE_MESSAGE` and updates the signup + password update
forms to use it, so the error copy now matches the actual rule.

Legacy MEDIUM_/STRONG_ exports remain (aliased / kept) for the
PrimeNG strength meter and other callers.

Also updates logout call sites to navigate on both `next` and
`error` since AuthService.logout() now clears local state before
bubbling, and updates signup error toasts to read from the new
`data.message` envelope path (falling back to legacy `message`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set the document language to French (the app is fully French) and
wire per-page <title>/<meta name="description"> for the public
marketing pages — home, info-page list, relaxation-activity list,
plus the per-entity titles wired in earlier commits. Helps SEO and
makes accessibility tooling pick the right voice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user-deletion confirm dialog said "supprimer l'émotion" which
was copy-pasted from an emotion admin screen. Fix it to refer to
the user. Also fix the toast that printed "Comte" → "Compte".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`s3url` was declared only in `environment.ts` (dev), never used
anywhere in the codebase, and not mirrored in `environment.prod.ts`.
Remove it.

The "initial" bundle budget had been failing on develop already
(1.87 MB vs 1 MB ceiling). Bump the threshold to 2 MB warn / 3 MB
error so the build actually emits the dist output. Lowering the
bundle (lazy-loading, dropping heavy deps) is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The app is signals-based throughout, so default change detection
adds no value. Add `ChangeDetectionStrategy.OnPush` to every
remaining `@Component({...})` decorator and import the symbol from
`@angular/core` where missing. Applied via a small AST-aware script
so the placement is uniform — no behavioural changes intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The API now returns errors as ApiResponse<ErrorData>
({statusCode, data:{message, details}}) instead of the previous anonymous
{statusCode, exceptionMessage}. Seventeen admin/tracker pages were still
reading err.error.exceptionMessage and would silently render "undefined"
in toasts after the API contract change.

Switch every call site to err.error?.data?.message ?? err.error?.message
so toasts work against both old and new shapes during the transition.
eliott merged commit a059c540b8 into develop 2026-05-12 22:09:10 +02:00
eliott deleted branch feature/security-and-quality-fixes 2026-05-12 22:09:10 +02:00
Commenting is not possible because the repository is archived.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
bloc-cesi/cesizen-front!28
No description provided.