refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)

* refactor(auth): replace dashboard sessions with stateless tokens

* feat(auth): harden session issuance and distributed enforcement

* fix(proxy): preserve trusted proxy compatibility defaults

* refactor: address dashboard auth review feedback

* refactor: remove classic frontend and flatten web app
This commit is contained in:
Calcium-Ion
2026-07-20 16:48:43 +08:00
committed by GitHub
parent 5a6c53d496
commit 31d70fca39
1605 changed files with 17511 additions and 147913 deletions
@@ -0,0 +1,225 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import * as z from 'zod'
import { Dialog } from '@/components/dialog'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const rateLimitDialogSchema = z.object({
groupName: z.string().min(1, 'Group name is required'),
maxRequests: z
.number()
.min(0, 'Must be ≥ 0')
.max(2147483647, 'Must be ≤ 2,147,483,647'),
maxSuccess: z
.number()
.min(1, 'Must be ≥ 1')
.max(2147483647, 'Must be ≤ 2,147,483,647'),
})
type RateLimitDialogFormValues = z.infer<typeof rateLimitDialogSchema>
const RATE_LIMIT_FORM_ID = 'rate-limit-form'
export type RateLimitEntryData = {
groupName: string
maxRequests: number
maxSuccess: number
}
type RateLimitDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
onSave: (data: RateLimitEntryData) => void
editData?: RateLimitEntryData | null
}
export function RateLimitDialog({
open,
onOpenChange,
onSave,
editData,
}: RateLimitDialogProps) {
const { t } = useTranslation()
const isEditMode = !!editData
const form = useForm<RateLimitDialogFormValues>({
resolver: zodResolver(rateLimitDialogSchema),
defaultValues: {
groupName: '',
maxRequests: 0,
maxSuccess: 1,
},
})
useEffect(() => {
if (editData) {
form.reset(editData)
} else {
form.reset({
groupName: '',
maxRequests: 0,
maxSuccess: 1,
})
}
}, [editData, form, open])
const handleSubmit = (values: RateLimitDialogFormValues) => {
onSave(values)
form.reset()
onOpenChange(false)
}
return (
<Dialog
open={open}
onOpenChange={onOpenChange}
title={
isEditMode ? t('Edit group rate limit') : t('Add group rate limit')
}
description={t(
'Configure rate limiting rules for a specific user group.'
)}
contentClassName='sm:max-w-[500px]'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='submit' form={RATE_LIMIT_FORM_ID}>
{isEditMode ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}>
<form
id={RATE_LIMIT_FORM_ID}
onSubmit={form.handleSubmit(handleSubmit)}
className='space-y-4'
>
<FormField
control={form.control}
name='groupName'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Group Name')}</FormLabel>
<FormControl>
<Input
placeholder={t('e.g., default, vip, premium')}
{...field}
disabled={isEditMode}
/>
</FormControl>
<FormDescription>
{isEditMode
? t('Group name cannot be changed when editing.')
: t('Unique identifier for this group.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='maxRequests'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Max Requests (including failures)')}</FormLabel>
<FormControl>
<div className='flex items-center gap-2'>
<Input
type='number'
min={0}
max={2147483647}
step={1}
{...field}
onChange={(e) =>
field.onChange(parseInt(e.target.value) || 0)
}
/>
<span className='text-muted-foreground text-sm'>
{t('times')}
</span>
</div>
</FormControl>
<FormDescription>
{t('Total requests allowed per period. 0 = unlimited.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='maxSuccess'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Max Successful Requests')}</FormLabel>
<FormControl>
<div className='flex items-center gap-2'>
<Input
type='number'
min={1}
max={2147483647}
step={1}
{...field}
onChange={(e) =>
field.onChange(parseInt(e.target.value) || 1)
}
/>
<span className='text-muted-foreground text-sm'>
{t('times')}
</span>
</div>
</FormControl>
<FormDescription>
{t('Only successful requests count toward this limit.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</Dialog>
)
}
@@ -0,0 +1,319 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { zodResolver } from '@hookform/resolvers/zod'
import { Code2, Palette } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
import { RateLimitVisualEditor } from './rate-limit-visual-editor'
const isValidJSON = (value: string | undefined) => {
if (!value || value.trim() === '') return true
try {
const parsed = JSON.parse(value)
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
return false
}
for (const [, val] of Object.entries(parsed)) {
if (!Array.isArray(val) || val.length !== 2) return false
if (typeof val[0] !== 'number' || typeof val[1] !== 'number') return false
if (val[0] < 0 || val[1] < 1) return false
if (val[0] > 2147483647 || val[1] > 2147483647) return false
}
return true
} catch {
return false
}
}
const createRateLimitSchema = (t: (key: string) => string) =>
z.object({
ModelRequestRateLimitEnabled: z.boolean(),
ModelRequestRateLimitDurationMinutes: z.number().min(0),
ModelRequestRateLimitCount: z.number().min(0).max(100000000),
ModelRequestRateLimitSuccessCount: z.number().min(1).max(100000000),
ModelRequestRateLimitGroup: z
.string()
.optional()
.refine(isValidJSON, {
message: t('Invalid JSON format or values out of allowed range'),
}),
})
type RateLimitFormValues = z.infer<ReturnType<typeof createRateLimitSchema>>
type RateLimitSectionProps = {
defaultValues: RateLimitFormValues
}
export function RateLimitSection({ defaultValues }: RateLimitSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const [useVisualEditor, setUseVisualEditor] = useState(true)
const rateLimitSchema = createRateLimitSchema(t)
const form = useForm<RateLimitFormValues>({
resolver: zodResolver(rateLimitSchema),
mode: 'onChange', // Enable real-time validation
defaultValues,
})
useEffect(() => {
form.reset(defaultValues)
}, [defaultValues, form])
const onSubmit = async (values: RateLimitFormValues) => {
const updates = Object.entries(values).filter(
([key, value]) =>
value !== defaultValues[key as keyof RateLimitFormValues]
)
for (const [key, value] of updates) {
await updateOption.mutateAsync({ key, value: value ?? '' })
}
}
return (
<SettingsSection title={t('Rate Limiting')}>
<Form {...form}>
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save rate limits'
/>
<FormField
control={form.control}
name='ModelRequestRateLimitEnabled'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Enable rate limiting')}</FormLabel>
<FormDescription>
{t(
'This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
<div className='grid gap-4 md:grid-cols-3'>
<FormField
control={form.control}
name='ModelRequestRateLimitDurationMinutes'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Limit period')}</FormLabel>
<FormControl>
<div className='flex items-center gap-2'>
<Input
type='number'
min={0}
step={1}
{...field}
onChange={(e) =>
field.onChange(parseInt(e.target.value) || 0)
}
/>
<span className='text-muted-foreground text-sm'>
{t('minutes')}
</span>
</div>
</FormControl>
<FormDescription>
{t('Time window for rate limiting')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='ModelRequestRateLimitCount'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Max requests per period')}</FormLabel>
<FormControl>
<div className='flex items-center gap-2'>
<Input
type='number'
min={0}
max={100000000}
step={1}
{...field}
onChange={(e) =>
field.onChange(parseInt(e.target.value) || 0)
}
/>
<span className='text-muted-foreground text-sm'>
{t('times')}
</span>
</div>
</FormControl>
<FormDescription>
{t('Including failed requests, 0 = unlimited')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='ModelRequestRateLimitSuccessCount'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Max successful requests')}</FormLabel>
<FormControl>
<div className='flex items-center gap-2'>
<Input
type='number'
min={1}
max={100000000}
step={1}
{...field}
onChange={(e) =>
field.onChange(parseInt(e.target.value) || 1)
}
/>
<span className='text-muted-foreground text-sm'>
{t('times')}
</span>
</div>
</FormControl>
<FormDescription>
{t('Only successful requests')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='ModelRequestRateLimitGroup'
render={({ field }) => (
<FormItem>
<div className='flex items-center justify-between'>
<FormLabel>{t('Group-based rate limits')}</FormLabel>
<Button
type='button'
variant='outline'
size='sm'
onClick={() => setUseVisualEditor(!useVisualEditor)}
>
{useVisualEditor ? (
<>
<Code2 className='mr-2 h-4 w-4' />
{t('JSON Mode')}
</>
) : (
<>
<Palette className='mr-2 h-4 w-4' />
{t('Visual Mode')}
</>
)}
</Button>
</div>
<FormControl>
{useVisualEditor ? (
<RateLimitVisualEditor
value={field.value || ''}
onChange={field.onChange}
/>
) : (
<Textarea
rows={8}
placeholder={`{\n "default": [200, 100],\n "vip": [0, 1000]\n}`}
className='font-mono text-sm'
{...field}
/>
)}
</FormControl>
{!useVisualEditor && (
<FormDescription>
<div className='space-y-1 text-xs'>
<p className='font-semibold'>{t('Format:')}</p>
<ul className='list-inside list-disc space-y-0.5 pl-2'>
<li>
{t('JSON object:')}{' '}
{`{"groupName": [maxRequests, maxSuccess]}`}
</li>
<li>
{t('Example:')}{' '}
{`{"default": [200, 100], "vip": [0, 1000]}`}
</li>
<li>
{t(
'maxRequests ≥ 0, maxSuccess ≥ 1, both ≤ 2,147,483,647'
)}
</li>
<li>
{t(
'Group config overrides global limits, shares the same period'
)}
</li>
</ul>
</div>
</FormDescription>
)}
<FormMessage />
</FormItem>
)}
/>
</SettingsForm>
</Form>
</SettingsSection>
)
}
@@ -0,0 +1,208 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Plus, Search } from 'lucide-react'
import { useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
import { StaticRowActions } from '@/components/data-table/static/static-row-actions'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { safeJsonParseWithValidation } from '../utils/json-parser'
import { isObjectRecord } from '../utils/json-validators'
import { RateLimitDialog, type RateLimitEntryData } from './rate-limit-dialog'
type RateLimitVisualEditorProps = {
value: string
onChange: (value: string) => void
}
type RateLimitEntry = RateLimitEntryData
export function RateLimitVisualEditor({
value,
onChange,
}: RateLimitVisualEditorProps) {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [dialogOpen, setDialogOpen] = useState(false)
const [editData, setEditData] = useState<RateLimitEntry | null>(null)
const rateLimits = useMemo(() => {
if (!value || value.trim() === '') return []
const parsed = safeJsonParseWithValidation<Record<string, unknown>>(value, {
fallback: {},
validator: isObjectRecord,
validatorMessage: 'Rate limits must be a JSON object',
context: 'rate limits',
})
return Object.entries(parsed)
.map(([groupName, limits]) => {
if (
Array.isArray(limits) &&
limits.length === 2 &&
typeof limits[0] === 'number' &&
typeof limits[1] === 'number'
) {
return {
groupName,
maxRequests: limits[0],
maxSuccess: limits[1],
}
}
return null
})
.filter((item): item is RateLimitEntry => item !== null)
}, [value])
const filteredRateLimits = useMemo(() => {
if (!searchText) return rateLimits
const lowerSearch = searchText.toLowerCase()
return rateLimits.filter((limit) =>
limit.groupName.toLowerCase().includes(lowerSearch)
)
}, [rateLimits, searchText])
const handleSave = (data: RateLimitEntryData) => {
const parsed = safeJsonParseWithValidation<Record<string, unknown>>(value, {
fallback: {},
validator: isObjectRecord,
silent: true,
})
if (editData && editData.groupName !== data.groupName) {
delete parsed[editData.groupName]
}
parsed[data.groupName] = [data.maxRequests, data.maxSuccess]
onChange(JSON.stringify(parsed, null, 2))
}
const handleDelete = (groupName: string) => {
const parsed = safeJsonParseWithValidation<Record<string, unknown>>(value, {
fallback: {},
validator: isObjectRecord,
silent: true,
})
delete parsed[groupName]
onChange(JSON.stringify(parsed, null, 2))
}
const handleEdit = (limit: RateLimitEntry) => {
setEditData(limit)
setDialogOpen(true)
}
const handleAdd = () => {
setEditData(null)
setDialogOpen(true)
}
return (
<div className='space-y-4'>
<div className='flex items-center gap-4'>
<div className='relative flex-1'>
<Search className='text-muted-foreground absolute top-2.5 left-2.5 h-4 w-4' />
<Input
placeholder={t('Search group names...')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className='pl-9'
/>
</div>
<Button onClick={handleAdd}>
<Plus className='mr-2 h-4 w-4' />
{t('Add group')}
</Button>
</div>
<StaticDataTable
data={filteredRateLimits}
getRowKey={(limit) => limit.groupName}
emptyContent={
searchText
? t('No groups match your search')
: t(
'No group-based rate limits configured. Click "Add group" to get started.'
)
}
columns={[
{
id: 'group',
header: t('Group Name'),
cellClassName: 'font-medium',
cell: (limit) => limit.groupName,
},
{
id: 'max-requests',
header: t('Max Requests (incl. failures)'),
className: 'text-right',
cellClassName: 'text-right',
cell: (limit) => (
<span className='font-mono'>
{limit.maxRequests === 0
? t('Unlimited')
: limit.maxRequests.toLocaleString()}
</span>
),
},
{
id: 'max-success',
header: t('Max Success'),
className: 'text-right',
cellClassName: 'text-right',
cell: (limit) => (
<span className='font-mono'>
{limit.maxSuccess.toLocaleString()}
</span>
),
},
{
id: 'actions',
header: t('Actions'),
className: 'text-right',
cellClassName: 'text-right',
cell: (limit) => (
<StaticRowActions
editLabel={t('Edit')}
deleteLabel={t('Delete')}
menuLabel={t('Open menu')}
onEdit={() => handleEdit(limit)}
onDelete={() => handleDelete(limit.groupName)}
/>
),
},
]}
/>
<RateLimitDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
onSave={handleSave}
editData={editData}
/>
</div>
)
}
@@ -0,0 +1,166 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import * as z from 'zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
const sensitiveSchema = z.object({
CheckSensitiveEnabled: z.boolean(),
CheckSensitiveOnPromptEnabled: z.boolean(),
SensitiveWords: z.string().optional(),
})
type SensitiveFormValues = z.infer<typeof sensitiveSchema>
type SensitiveWordsSectionProps = {
defaultValues: SensitiveFormValues
}
export function SensitiveWordsSection({
defaultValues,
}: SensitiveWordsSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const form = useForm<SensitiveFormValues>({
resolver: zodResolver(sensitiveSchema),
defaultValues,
})
useEffect(() => {
form.reset(defaultValues)
}, [defaultValues, form])
const onSubmit = async (values: SensitiveFormValues) => {
const updates = Object.entries(values).filter(
([key, value]) =>
value !== defaultValues[key as keyof SensitiveFormValues]
)
for (const [key, value] of updates) {
await updateOption.mutateAsync({ key, value: value ?? '' })
}
}
return (
<SettingsSection title={t('Sensitive Words')}>
<Form {...form}>
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save sensitive words'
/>
<div className='space-y-4'>
<FormField
control={form.control}
name='CheckSensitiveEnabled'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Enable filtering')}</FormLabel>
<FormDescription>
{t(
'Blocks messages when sensitive keywords are detected.'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
<FormField
control={form.control}
name='CheckSensitiveOnPromptEnabled'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Inspect user prompts')}</FormLabel>
<FormDescription>
{t(
'When enabled, prompts are scanned before reaching upstream models.'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
</div>
<FormField
control={form.control}
name='SensitiveWords'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Blocked keywords')}</FormLabel>
<FormControl>
<Textarea
rows={12}
placeholder={t('Enter one keyword per line')}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Each line represents one keyword. Leave blank to disable the list but keep the switch states.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsForm>
</Form>
</SettingsSection>
)
}
@@ -0,0 +1,438 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useMemo, useRef } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import * as z from 'zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
const ssrfSchema = z.object({
fetch_setting: z.object({
enable_ssrf_protection: z.boolean(),
allow_private_ip: z.boolean(),
domain_filter_mode: z.boolean(),
ip_filter_mode: z.boolean(),
domain_list: z.string(),
ip_list: z.string(),
allowed_ports: z.string(),
apply_ip_filter_for_domain: z.boolean(),
}),
})
type SSRFFormValues = z.output<typeof ssrfSchema>
type SSRFFormInput = z.input<typeof ssrfSchema>
type NormalizedSSRFValues = {
'fetch_setting.enable_ssrf_protection': boolean
'fetch_setting.allow_private_ip': boolean
'fetch_setting.domain_filter_mode': boolean
'fetch_setting.ip_filter_mode': boolean
'fetch_setting.domain_list': string[]
'fetch_setting.ip_list': string[]
'fetch_setting.allowed_ports': number[]
'fetch_setting.apply_ip_filter_for_domain': boolean
}
type SSRFSectionProps = {
defaultValues: {
'fetch_setting.enable_ssrf_protection': boolean
'fetch_setting.allow_private_ip': boolean
'fetch_setting.domain_filter_mode': boolean
'fetch_setting.ip_filter_mode': boolean
'fetch_setting.domain_list': string[]
'fetch_setting.ip_list': string[]
'fetch_setting.allowed_ports': number[]
'fetch_setting.apply_ip_filter_for_domain': boolean
}
}
const splitLines = (value: string) =>
value
.split('\n')
.map((entry) => entry.trim())
.filter(Boolean)
const parsePorts = (value: string) =>
value
.split(',')
.map((item) => Number.parseInt(item.trim(), 10))
.filter((port) => Number.isFinite(port))
const buildFormDefaults = (
defaults: SSRFSectionProps['defaultValues']
): SSRFFormInput => ({
fetch_setting: {
enable_ssrf_protection: defaults['fetch_setting.enable_ssrf_protection'],
allow_private_ip: defaults['fetch_setting.allow_private_ip'],
domain_filter_mode: defaults['fetch_setting.domain_filter_mode'],
ip_filter_mode: defaults['fetch_setting.ip_filter_mode'],
domain_list: defaults['fetch_setting.domain_list'].join('\n'),
ip_list: defaults['fetch_setting.ip_list'].join('\n'),
allowed_ports: defaults['fetch_setting.allowed_ports'].join(','),
apply_ip_filter_for_domain:
defaults['fetch_setting.apply_ip_filter_for_domain'],
},
})
const normalizeDefaults = (
defaults: SSRFSectionProps['defaultValues']
): NormalizedSSRFValues => ({
'fetch_setting.enable_ssrf_protection':
defaults['fetch_setting.enable_ssrf_protection'],
'fetch_setting.allow_private_ip': defaults['fetch_setting.allow_private_ip'],
'fetch_setting.domain_filter_mode':
defaults['fetch_setting.domain_filter_mode'],
'fetch_setting.ip_filter_mode': defaults['fetch_setting.ip_filter_mode'],
'fetch_setting.domain_list': defaults['fetch_setting.domain_list'],
'fetch_setting.ip_list': defaults['fetch_setting.ip_list'],
'fetch_setting.allowed_ports': defaults['fetch_setting.allowed_ports'],
'fetch_setting.apply_ip_filter_for_domain':
defaults['fetch_setting.apply_ip_filter_for_domain'],
})
const normalizeFormValues = (values: SSRFFormValues): NormalizedSSRFValues => ({
'fetch_setting.enable_ssrf_protection':
values.fetch_setting.enable_ssrf_protection,
'fetch_setting.allow_private_ip': values.fetch_setting.allow_private_ip,
'fetch_setting.domain_filter_mode': values.fetch_setting.domain_filter_mode,
'fetch_setting.ip_filter_mode': values.fetch_setting.ip_filter_mode,
'fetch_setting.domain_list': splitLines(values.fetch_setting.domain_list),
'fetch_setting.ip_list': splitLines(values.fetch_setting.ip_list),
'fetch_setting.allowed_ports': parsePorts(values.fetch_setting.allowed_ports),
'fetch_setting.apply_ip_filter_for_domain':
values.fetch_setting.apply_ip_filter_for_domain,
})
const isEqual = (a: unknown, b: unknown) => {
if (Array.isArray(a) && Array.isArray(b)) {
return JSON.stringify(a) === JSON.stringify(b)
}
return a === b
}
export function SSRFSection({ defaultValues }: SSRFSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const baselineRef = useRef<NormalizedSSRFValues>(
normalizeDefaults(defaultValues)
)
const formDefaults = useMemo(
() => buildFormDefaults(defaultValues),
[defaultValues]
)
const form = useForm<SSRFFormInput, unknown, SSRFFormValues>({
resolver: zodResolver(ssrfSchema),
defaultValues: formDefaults,
})
useEffect(() => {
baselineRef.current = normalizeDefaults(defaultValues)
form.reset(buildFormDefaults(defaultValues))
}, [defaultValues, form])
const onSubmit = async (data: SSRFFormValues) => {
const normalized = normalizeFormValues(data)
const updates = (
Object.keys(normalized) as Array<keyof NormalizedSSRFValues>
).filter((key) => !isEqual(normalized[key], baselineRef.current[key]))
if (updates.length === 0) {
toast.info(t('No changes to save'))
return
}
for (const key of updates) {
const value = normalized[key]
await updateOption.mutateAsync({
key,
value: Array.isArray(value) ? JSON.stringify(value) : value,
})
}
baselineRef.current = normalized
}
const domainFilterMode = form.watch('fetch_setting.domain_filter_mode')
const ipFilterMode = form.watch('fetch_setting.ip_filter_mode')
return (
<SettingsSection title={t('SSRF Protection')}>
<Form {...form}>
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save SSRF settings'
/>
<FormField
control={form.control}
name='fetch_setting.enable_ssrf_protection'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Enable SSRF Protection')}</FormLabel>
<FormDescription>
{t('Prevent server-side request forgery attacks')}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
<FormField
control={form.control}
name='fetch_setting.allow_private_ip'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Allow Private IPs')}</FormLabel>
<FormDescription>
{t(
'Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
<FormField
control={form.control}
name='fetch_setting.domain_filter_mode'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Domain Filter Mode')}</FormLabel>
<Select
items={[
{
value: 'false',
label: t('Blacklist (Block listed domains)'),
},
{
value: 'true',
label: t('Whitelist (Only allow listed domains)'),
},
]}
onValueChange={(value) => field.onChange(value === 'true')}
value={field.value ? 'true' : 'false'}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent alignItemWithTrigger={false}>
<SelectGroup>
<SelectItem value='false'>
{t('Blacklist (Block listed domains)')}
</SelectItem>
<SelectItem value='true'>
{t('Whitelist (Only allow listed domains)')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
{t('Choose how to filter domains')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='fetch_setting.domain_list'
render={({ field }) => (
<FormItem>
<FormLabel>
{t('Domain')}{' '}
{domainFilterMode ? t('Whitelist') : t('Blacklist')}
</FormLabel>
<FormControl>
<Textarea
placeholder={t('example.com&#10;blocked-site.com')}
rows={4}
{...field}
/>
</FormControl>
<FormDescription>{t('One domain per line')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='fetch_setting.ip_filter_mode'
render={({ field }) => (
<FormItem>
<FormLabel>{t('IP Filter Mode')}</FormLabel>
<Select
items={[
{
value: 'false',
label: t('Blacklist (Block listed IPs)'),
},
{
value: 'true',
label: t('Whitelist (Only allow listed IPs)'),
},
]}
onValueChange={(value) => field.onChange(value === 'true')}
value={field.value ? 'true' : 'false'}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent alignItemWithTrigger={false}>
<SelectGroup>
<SelectItem value='false'>
{t('Blacklist (Block listed IPs)')}
</SelectItem>
<SelectItem value='true'>
{t('Whitelist (Only allow listed IPs)')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
{t('Choose how to filter IP addresses')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='fetch_setting.ip_list'
render={({ field }) => (
<FormItem>
<FormLabel>
{t('IP')} {ipFilterMode ? t('Whitelist') : t('Blacklist')}
</FormLabel>
<FormControl>
<Textarea
placeholder={t('192.168.1.1&#10;10.0.0.0/8')}
rows={4}
{...field}
/>
</FormControl>
<FormDescription>
{t('One IP or CIDR range per line')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='fetch_setting.allowed_ports'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Allowed Ports')}</FormLabel>
<FormControl>
<Input placeholder={t('80,443,8080')} {...field} />
</FormControl>
<FormDescription>
{t(
'Comma-separated list of allowed ports (empty = all ports)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='fetch_setting.apply_ip_filter_for_domain'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>
{t('Apply IP Filter to Resolved Domains')}
</FormLabel>
<FormDescription>
{t(
'Check resolved IPs against IP filters even when accessing by domain'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
</SettingsForm>
</Form>
</SettingsSection>
)
}
@@ -0,0 +1,133 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import * as z from 'zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { SettingsForm } from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
const tokenLimitSchema = z.object({
token_setting: z.object({
max_user_tokens: z.number().min(1),
}),
})
type TokenLimitFormValues = z.output<typeof tokenLimitSchema>
type TokenLimitFormInput = z.input<typeof tokenLimitSchema>
type NormalizedTokenLimitValues = {
'token_setting.max_user_tokens': number
}
type TokenLimitSectionProps = {
defaultValues: NormalizedTokenLimitValues
}
const buildFormDefaults = (
defaults: TokenLimitSectionProps['defaultValues']
): TokenLimitFormInput => ({
token_setting: {
max_user_tokens: defaults['token_setting.max_user_tokens'],
},
})
const normalizeFormValues = (
values: TokenLimitFormValues
): NormalizedTokenLimitValues => ({
'token_setting.max_user_tokens': values.token_setting.max_user_tokens,
})
export function TokenLimitSection({ defaultValues }: TokenLimitSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const form = useForm<TokenLimitFormInput, unknown, TokenLimitFormValues>({
resolver: zodResolver(tokenLimitSchema),
mode: 'onChange',
defaultValues: buildFormDefaults(defaultValues),
})
useEffect(() => {
form.reset(buildFormDefaults(defaultValues))
}, [defaultValues, form])
const onSubmit = async (values: TokenLimitFormValues) => {
const key = 'token_setting.max_user_tokens' as const
const normalized = normalizeFormValues(values)
const value = normalized[key]
if (value !== defaultValues[key]) {
await updateOption.mutateAsync({ key, value })
}
}
return (
<SettingsSection title={t('Token Limits')}>
<Form {...form}>
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save token limits'
/>
<FormField
control={form.control}
name='token_setting.max_user_tokens'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Maximum tokens per user')}</FormLabel>
<FormControl>
<Input
type='number'
min={1}
step={1}
{...field}
onChange={(e) =>
field.onChange(Number.parseInt(e.target.value) || 1)
}
/>
</FormControl>
<FormDescription>
{t(
'Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsForm>
</Form>
</SettingsSection>
)
}