mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
fc7d8b5d12
Brownfield strangler migration of the backend onto NestJS modules (auth, trips, days, places, assignments, packing, todo, budget, reservations, collab, files, photos, journey, share, settings, backup, oidc, oauth, admin, atlas, vacay, weather, airports, maps, categories, tags, notifications, system-notices) served through a per-prefix dispatcher, keeping the existing SQLite/better-sqlite3 DB and JWT httpOnly cookie auth, with behavioural parity for every route. Client: React 19 upgrade, "page = wiring container + data hook" pattern across all pages, per-domain Zustand stores bound to @trek/shared contracts, and decomposition of the large components (DayPlanSidebar, PackingListPanel, CollabNotes, FileManager, MemoriesPanel, PlacesSidebar, CollabChat, SystemNoticeModal, BudgetPanel, PlaceFormModal, ...) into focused render units backed by in-file hooks. Apply the shared global request pipeline (helmet/CSP, CORS, HSTS, forced HTTPS, the global MFA policy and request logging) to the NestJS instance as well, so a migrated route is protected identically to the legacy fallback rather than bypassing it.
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
import type { JSX } from 'react'
|
|
import { useTranslation } from './TranslationContext'
|
|
|
|
interface TransHtmlProps<T extends keyof JSX.IntrinsicElements = 'span'> {
|
|
/**
|
|
* Translation key whose template legitimately contains markup (e.g.
|
|
* `'Turn <strong>{title}</strong> into a Journey'`).
|
|
*/
|
|
html: string
|
|
/**
|
|
* Values to interpolate into `{paramName}` placeholders. Every value is
|
|
* HTML-escaped before substitution, so passing user-controlled data is safe.
|
|
*/
|
|
params?: Record<string, string | number>
|
|
/**
|
|
* Element to render. Defaults to `<span>`. Use the tag that fits the
|
|
* surrounding flow — block, inline, list item, etc.
|
|
*/
|
|
as?: T
|
|
className?: string
|
|
/**
|
|
* `id` is forwarded so the component can be the target of `aria-labelledby`
|
|
* or `htmlFor`. Other ARIA attributes can be added if needed; we intentionally
|
|
* keep the surface small to discourage overloading this with arbitrary props.
|
|
*/
|
|
id?: string
|
|
}
|
|
|
|
/**
|
|
* Renders a translation that contains markup (e.g. `<strong>`) safely.
|
|
*
|
|
* Replaces the pattern that bit us in the Journey suggestion banner:
|
|
* <span dangerouslySetInnerHTML={{ __html: t('...', { user_input }) }} />
|
|
*
|
|
* That pattern interpolates `user_input` into the template *before* React
|
|
* ever sees it, so a trip title like `<script>alert(1)</script>` would inject
|
|
* a script tag. `TransHtml` runs `tHtml()` which:
|
|
*
|
|
* 1. HTML-escapes every interpolated value, neutralising it.
|
|
* 2. Sanitises the resulting string against an inline tag allow-list.
|
|
*
|
|
* Use this for any user-controlled value that lands in a markup template.
|
|
* Plain text-only templates can continue to use `<>{t('key', params)}</>`.
|
|
*/
|
|
export function TransHtml<T extends keyof JSX.IntrinsicElements = 'span'>({
|
|
html,
|
|
params,
|
|
as,
|
|
className,
|
|
id,
|
|
}: TransHtmlProps<T>) {
|
|
const { tHtml } = useTranslation()
|
|
const Tag = (as ?? 'span') as keyof JSX.IntrinsicElements
|
|
return (
|
|
// eslint-disable-next-line react/no-danger -- sanitised by tHtml (defence in depth)
|
|
<Tag className={className} id={id} dangerouslySetInnerHTML={{ __html: tHtml(html, params) }} />
|
|
)
|
|
}
|