mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-06 17:46:23 +00:00
fix(web): secure rich content rendering
This commit is contained in:
+11
-2
@@ -16,6 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface HtmlContentProps {
|
interface HtmlContentProps {
|
||||||
@@ -24,10 +27,16 @@ interface HtmlContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HtmlContent(props: HtmlContentProps) {
|
export function HtmlContent(props: HtmlContentProps) {
|
||||||
|
const html = useMemo(() => DOMPurify.sanitize(props.content), [props.content])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('prose prose-neutral dark:prose-invert max-w-none', props.className)}
|
className={cn(
|
||||||
dangerouslySetInnerHTML={{ __html: props.content }}
|
'prose prose-neutral dark:prose-invert max-w-none',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
// eslint-disable-next-line react/no-danger -- html is sanitized above
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -16,18 +16,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { isLikelyHtml } from '@/lib/content-format'
|
|
||||||
import { HtmlContent } from '@/components/html-content'
|
import { HtmlContent } from '@/components/html-content'
|
||||||
import { Markdown } from '@/components/ui/markdown'
|
import { Markdown } from '@/components/ui/markdown'
|
||||||
|
|
||||||
|
type RichContentMode = 'markdown' | 'html'
|
||||||
|
|
||||||
interface RichContentProps {
|
interface RichContentProps {
|
||||||
content: string
|
content: string
|
||||||
|
mode?: RichContentMode
|
||||||
breaks?: boolean
|
breaks?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RichContent(props: RichContentProps) {
|
export function RichContent(props: RichContentProps) {
|
||||||
if (isLikelyHtml(props.content)) {
|
if (props.mode === 'html') {
|
||||||
return <HtmlContent content={props.content} className={props.className} />
|
return <HtmlContent content={props.content} className={props.className} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -19,10 +19,12 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Construction } from 'lucide-react'
|
import { Construction } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { PublicLayout } from '@/components/layout'
|
||||||
import { RichContent } from '@/components/rich-content'
|
import { RichContent } from '@/components/rich-content'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { PublicLayout } from '@/components/layout'
|
import { isHttpUrl, isLikelyHtml } from '@/lib/content-format'
|
||||||
import { isHttpUrl } from '@/lib/content-format'
|
|
||||||
import { getAboutContent } from './api'
|
import { getAboutContent } from './api'
|
||||||
|
|
||||||
function EmptyAboutState() {
|
function EmptyAboutState() {
|
||||||
@@ -159,6 +161,7 @@ export function About() {
|
|||||||
<PublicLayout>
|
<PublicLayout>
|
||||||
<div className='mx-auto max-w-6xl px-4 py-8'>
|
<div className='mx-auto max-w-6xl px-4 py-8'>
|
||||||
<RichContent
|
<RichContent
|
||||||
|
mode={isLikelyHtml(rawContent) ? 'html' : 'markdown'}
|
||||||
content={rawContent}
|
content={rawContent}
|
||||||
className='prose-neutral dark:prose-invert max-w-none'
|
className='prose-neutral dark:prose-invert max-w-none'
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-3
@@ -17,10 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuthStore } from '@/stores/auth-store'
|
|
||||||
import { RichContent } from '@/components/rich-content'
|
|
||||||
import { PublicLayout } from '@/components/layout'
|
import { PublicLayout } from '@/components/layout'
|
||||||
import { Footer } from '@/components/layout/components/footer'
|
import { Footer } from '@/components/layout/components/footer'
|
||||||
|
import { RichContent } from '@/components/rich-content'
|
||||||
|
import { isLikelyHtml } from '@/lib/content-format'
|
||||||
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
|
|
||||||
import { CTA, Features, Hero, HowItWorks, Stats } from './components'
|
import { CTA, Features, Hero, HowItWorks, Stats } from './components'
|
||||||
import { useHomePageContent } from './hooks'
|
import { useHomePageContent } from './hooks'
|
||||||
|
|
||||||
@@ -57,7 +60,11 @@ export function Home() {
|
|||||||
return (
|
return (
|
||||||
<PublicLayout>
|
<PublicLayout>
|
||||||
<div className='mx-auto max-w-6xl px-4 py-8'>
|
<div className='mx-auto max-w-6xl px-4 py-8'>
|
||||||
<RichContent content={content} className='custom-home-content' />
|
<RichContent
|
||||||
|
mode={isLikelyHtml(content) ? 'html' : 'markdown'}
|
||||||
|
content={content}
|
||||||
|
className='custom-home-content'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PublicLayout>
|
</PublicLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-2
@@ -19,12 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { FileWarning } from 'lucide-react'
|
import { FileWarning } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { PublicLayout } from '@/components/layout'
|
||||||
import { RichContent } from '@/components/rich-content'
|
import { RichContent } from '@/components/rich-content'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { PublicLayout } from '@/components/layout'
|
import { isHttpUrl, isLikelyHtml } from '@/lib/content-format'
|
||||||
import { isHttpUrl } from '@/lib/content-format'
|
|
||||||
import type { LegalDocumentResponse } from './types'
|
import type { LegalDocumentResponse } from './types'
|
||||||
|
|
||||||
type LegalDocumentProps = {
|
type LegalDocumentProps = {
|
||||||
@@ -127,6 +129,7 @@ export function LegalDocument({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RichContent
|
<RichContent
|
||||||
|
mode={isLikelyHtml(rawContent) ? 'html' : 'markdown'}
|
||||||
content={rawContent}
|
content={rawContent}
|
||||||
className='prose-neutral dark:prose-invert max-w-none'
|
className='prose-neutral dark:prose-invert max-w-none'
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user