Skip to content

Commit 7e55e01

Browse files
committed
feat: add custom response headers support
Enables configuration of additional HTTP headers for subscription responses - Adds new UI component for managing custom response headers - Implements header management in subscription settings - Updates translations for English, Russian, and Persian - Updates backend contract to version 0.4.2 for header support
1 parent ea23c1a commit 7e55e01

File tree

8 files changed

+214
-13
lines changed

8 files changed

+214
-13
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"@mantine/nprogress": "^7.17.4",
4747
"@monaco-editor/react": "^4.7.0",
4848
"@paralleldrive/cuid2": "2.2.2",
49-
"@remnawave/backend-contract": "0.4.0",
49+
"@remnawave/backend-contract": "0.4.2",
5050
"@stablelib/base64": "^2.0.1",
5151
"@stablelib/x25519": "^2.0.1",
5252
"@tabler/icons-react": "^3.31.0",

public/locales/en/remnawave.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,9 @@
925925
"widget": {
926926
"show-custom-remarks": "Show custom remarks",
927927
"show-custom-remark-description-line-1": "Only for EXPIRED, LIMITED and DISABLED users.",
928-
"show-custom-remark-description-line-2": "If this setting is disabled – EXPIRED, LIMITED and DISABLED users will receive an actual host list, instead of the custom remarks below."
928+
"show-custom-remark-description-line-2": "If this setting is disabled – EXPIRED, LIMITED and DISABLED users will receive an actual host list, instead of the custom remarks below.",
929+
"additional-response-headers": "Additional response headers",
930+
"headers-that-will-be-sent-with-subscription-content": "Headers that will be sent with subscription content."
929931
}
930932
},
931933
"nodes-realtime-metrics": {
@@ -999,5 +1001,12 @@
9991001
"search-by-hwid": "Search by HWID",
10001002
"enter-hwid-to-filter-devices": "Enter HWID to filter devices..."
10011003
}
1004+
},
1005+
"headers-manager": {
1006+
"widget": {
1007+
"key": "Key",
1008+
"value": "Value",
1009+
"add-header": "Add header"
1010+
}
10021011
}
1003-
}
1012+
}

public/locales/fa/remnawave.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,9 @@
925925
"widget": {
926926
"show-custom-remarks": "نمایش توضیحات سفارشی",
927927
"show-custom-remark-description-line-1": "فقط برای کاربران منقضی‌شده، محدودشده و غیرفعال.",
928-
"show-custom-remark-description-line-2": "اگر این گزینه غیرفعال باشد - کاربران منقضی‌شده، محدودشده و غیرفعال به جای توضیحات سفارشی زیر، لیست واقعی هاست‌ها را دریافت خواهند کرد."
928+
"show-custom-remark-description-line-2": "اگر این گزینه غیرفعال باشد - کاربران منقضی‌شده، محدودشده و غیرفعال به جای توضیحات سفارشی زیر، لیست واقعی هاست‌ها را دریافت خواهند کرد.",
929+
"additional-response-headers": "Additional response headers",
930+
"headers-that-will-be-sent-with-subscription-content": "Headers that will be sent with subscription content."
929931
}
930932
},
931933
"nodes-realtime-metrics": {
@@ -999,5 +1001,12 @@
9991001
"unknown": "Unknown",
10001002
"user-agent": "User Agent"
10011003
}
1004+
},
1005+
"headers-manager": {
1006+
"widget": {
1007+
"add-header": "Add header",
1008+
"key": "Key",
1009+
"value": "Value"
1010+
}
10021011
}
1003-
}
1012+
}

public/locales/ru/remnawave.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,9 @@
925925
"widget": {
926926
"show-custom-remark-description-line-1": "Только для EXPIRED, LIMITED, DISABLED пользователей.",
927927
"show-custom-remark-description-line-2": "Если эта настройка отключена – EXPIRED, LIMITED, DISABLED пользователи получат актуальный список хостов, вместо кастомных примечаний ниже.",
928-
"show-custom-remarks": "Отображать кастомное примечание"
928+
"show-custom-remarks": "Отображать кастомное примечание",
929+
"additional-response-headers": "Доп. хэдеры",
930+
"headers-that-will-be-sent-with-subscription-content": "Хэдеры будут отправлены вместо с содержимым подписки."
929931
}
930932
},
931933
"nodes-realtime-metrics": {
@@ -999,5 +1001,12 @@
9991001
"unknown": "Неизвестно",
10001002
"user-agent": "Юзер-агент"
10011003
}
1004+
},
1005+
"headers-manager": {
1006+
"widget": {
1007+
"add-header": "Добавить хэдер",
1008+
"key": "Ключ",
1009+
"value": "Значение"
1010+
}
10021011
}
10031012
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { ActionIcon, Button, Group, TextInput } from '@mantine/core'
2+
import { PiPlus, PiTrash } from 'react-icons/pi'
3+
import { useTranslation } from 'react-i18next'
4+
import { useEffect, useState } from 'react'
5+
6+
export interface HeaderItem {
7+
key: string
8+
value: string
9+
}
10+
11+
export interface HeadersManagerProps {
12+
initialHeaders?: HeaderItem[]
13+
onChange: (headers: HeaderItem[]) => void
14+
}
15+
16+
export const HeadersManager = (props: HeadersManagerProps) => {
17+
const { initialHeaders = [{ key: '', value: '' }], onChange } = props
18+
19+
const [localHeaders, setLocalHeaders] = useState<HeaderItem[]>(initialHeaders)
20+
21+
const { t } = useTranslation()
22+
23+
useEffect(() => {
24+
if (JSON.stringify(initialHeaders) !== JSON.stringify(localHeaders)) {
25+
setLocalHeaders(initialHeaders)
26+
}
27+
}, [initialHeaders])
28+
29+
useEffect(() => {
30+
onChange(localHeaders)
31+
}, [localHeaders, onChange])
32+
33+
const addLocalHeader = () => {
34+
setLocalHeaders((prev) => [...prev, { key: '', value: '' }])
35+
}
36+
37+
const removeLocalHeader = (index: number) => {
38+
setLocalHeaders((prev) => {
39+
const newHeaders = [...prev]
40+
newHeaders.splice(index, 1)
41+
return newHeaders
42+
})
43+
}
44+
45+
const updateLocalHeaderKey = (index: number, key: string) => {
46+
setLocalHeaders((prev) => {
47+
const newHeaders = [...prev]
48+
newHeaders[index] = { ...newHeaders[index], key }
49+
return newHeaders
50+
})
51+
}
52+
53+
const updateLocalHeaderValue = (index: number, value: string) => {
54+
setLocalHeaders((prev) => {
55+
const newHeaders = [...prev]
56+
newHeaders[index] = { ...newHeaders[index], value }
57+
return newHeaders
58+
})
59+
}
60+
61+
return (
62+
<>
63+
{localHeaders.map((header, index) => (
64+
<Group align="flex-start" gap="sm" key={index} mb="xs">
65+
<ActionIcon
66+
color="red"
67+
onClick={() => removeLocalHeader(index)}
68+
radius="md"
69+
size="lg"
70+
variant="light"
71+
>
72+
<PiTrash size="1rem" />
73+
</ActionIcon>
74+
<TextInput
75+
onChange={(e) => updateLocalHeaderKey(index, e.target.value)}
76+
placeholder={t('headers-manager.widget.key')}
77+
style={{ flex: 1 }}
78+
value={header.key}
79+
/>
80+
<TextInput
81+
onChange={(e) => updateLocalHeaderValue(index, e.target.value)}
82+
placeholder={t('headers-manager.widget.value')}
83+
style={{ flex: 1 }}
84+
value={header.value}
85+
/>
86+
</Group>
87+
))}
88+
<Button
89+
leftSection={<PiPlus size="1rem" />}
90+
mt="xs"
91+
onClick={addLocalHeader}
92+
size="sm"
93+
variant="light"
94+
>
95+
{t('headers-manager.widget.add-header')}
96+
</Button>
97+
</>
98+
)
99+
}

src/widgets/dashboard/subscription-settings/settings/subscription-settings.widget.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
77
import { useUpdateSubscriptionSettings } from '@shared/api/hooks'
88

99
import { SubscriptionTabs } from './subscription-tabs.widget'
10+
import { HeaderItem } from './headers-manager.widget'
1011
import { IProps } from './interfaces'
1112

1213
export const SubscriptionSettingsWidget = (props: IProps) => {
@@ -19,6 +20,8 @@ export const SubscriptionSettingsWidget = (props: IProps) => {
1920
disabled: ['']
2021
})
2122

23+
const [headers, setHeaders] = useState<HeaderItem[]>([])
24+
2225
const form = useForm<UpdateSubscriptionSettingsCommand.Request>({
2326
name: 'edit-subscription-settings-form',
2427
mode: 'uncontrolled',
@@ -41,6 +44,10 @@ export const SubscriptionSettingsWidget = (props: IProps) => {
4144
setRemarks((prev) => ({ ...prev, disabled: newRemarks }))
4245
}, [])
4346

47+
const updateHeaders = useCallback((newHeaders: HeaderItem[]) => {
48+
setHeaders(newHeaders)
49+
}, [])
50+
4451
const { mutate: updateSubscriptionSettings, isPending: isUpdateSubscriptionSettingsPending } =
4552
useUpdateSubscriptionSettings({})
4653
useEffect(() => {
@@ -63,6 +70,19 @@ export const SubscriptionSettingsWidget = (props: IProps) => {
6370
disabled: processRemarks(subscriptionSettings.disabledUsersRemarks)
6471
})
6572

73+
if (
74+
subscriptionSettings.customResponseHeaders &&
75+
typeof subscriptionSettings.customResponseHeaders === 'object' &&
76+
subscriptionSettings.customResponseHeaders !== null
77+
) {
78+
const headerItems = Object.entries(subscriptionSettings.customResponseHeaders).map(
79+
([key, value]) => ({ key, value })
80+
)
81+
setHeaders(headerItems)
82+
} else {
83+
setHeaders([])
84+
}
85+
6686
form.setValues({
6787
uuid: subscriptionSettings.uuid,
6888
profileTitle: subscriptionSettings.profileTitle,
@@ -81,7 +101,8 @@ export const SubscriptionSettingsWidget = (props: IProps) => {
81101
disabledUsersRemarks: subscriptionSettings.disabledUsersRemarks,
82102
serveJsonAtBaseSubscription: subscriptionSettings.serveJsonAtBaseSubscription,
83103
addUsernameToBaseSubscription: subscriptionSettings.addUsernameToBaseSubscription,
84-
isShowCustomRemarks: subscriptionSettings.isShowCustomRemarks
104+
isShowCustomRemarks: subscriptionSettings.isShowCustomRemarks,
105+
customResponseHeaders: subscriptionSettings.customResponseHeaders || undefined
85106
})
86107
}
87108
}, [subscriptionSettings])
@@ -107,6 +128,13 @@ export const SubscriptionSettingsWidget = (props: IProps) => {
107128
return
108129
}
109130

131+
const headersFiltered = headers.filter((header) => header.key.trim() !== '')
132+
133+
const customResponseHeaders: Record<string, string> = {}
134+
headersFiltered.forEach((header) => {
135+
customResponseHeaders[header.key] = header.value
136+
})
137+
110138
const isProfileWebpageUrlEnabled =
111139
(values.isProfileWebpageUrlEnabled as unknown as string) === 'true'
112140

@@ -117,7 +145,8 @@ export const SubscriptionSettingsWidget = (props: IProps) => {
117145
isProfileWebpageUrlEnabled,
118146
expiredUsersRemarks: expiredFiltered,
119147
limitedUsersRemarks: limitedFiltered,
120-
disabledUsersRemarks: disabledFiltered
148+
disabledUsersRemarks: disabledFiltered,
149+
customResponseHeaders
121150
}
122151
})
123152
})
@@ -126,10 +155,12 @@ export const SubscriptionSettingsWidget = (props: IProps) => {
126155
<SubscriptionTabs
127156
form={form}
128157
handleSubmit={handleSubmit}
158+
headers={headers}
129159
isUpdateSubscriptionSettingsPending={isUpdateSubscriptionSettingsPending}
130160
remarks={remarks}
131161
updateDisabledRemarks={updateDisabledRemarks}
132162
updateExpiredRemarks={updateExpiredRemarks}
163+
updateHeaders={updateHeaders}
133164
updateLimitedRemarks={updateLimitedRemarks}
134165
/>
135166
)

src/widgets/dashboard/subscription-settings/settings/subscription-tabs.widget.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,26 @@ import {
2929
PiUserCircle
3030
} from 'react-icons/pi'
3131
import { UpdateSubscriptionSettingsCommand } from '@remnawave/backend-contract'
32+
import { TbPrescription } from 'react-icons/tb'
3233
import { useTranslation } from 'react-i18next'
3334
import { useNavigate } from 'react-router-dom'
3435
import { useForm } from '@mantine/form'
3536

3637
import { TemplateInfoPopoverShared } from '@shared/ui/popovers/template-info-popover/template-info-popover.shared'
3738
import { ROUTES } from '@shared/constants'
3839

40+
import { HeaderItem, HeadersManager } from './headers-manager.widget'
3941
import { RemarksManager } from './remarks-manager.widget'
4042

4143
interface SubscriptionTabsProps {
4244
form: ReturnType<typeof useForm<UpdateSubscriptionSettingsCommand.Request>>
4345
handleSubmit: () => void
46+
headers: HeaderItem[]
4447
isUpdateSubscriptionSettingsPending: boolean
4548
remarks: Record<string, string[]>
4649
updateDisabledRemarks: (newRemarks: string[]) => void
4750
updateExpiredRemarks: (newRemarks: string[]) => void
51+
updateHeaders: (newHeaders: HeaderItem[]) => void
4852
updateLimitedRemarks: (newRemarks: string[]) => void
4953
}
5054

@@ -55,7 +59,9 @@ export const SubscriptionTabs = ({
5559
updateLimitedRemarks,
5660
updateDisabledRemarks,
5761
handleSubmit,
58-
isUpdateSubscriptionSettingsPending
62+
isUpdateSubscriptionSettingsPending,
63+
headers,
64+
updateHeaders
5965
}: SubscriptionTabsProps) => {
6066
const { t } = useTranslation()
6167
const theme = useMantineTheme()
@@ -104,6 +110,14 @@ export const SubscriptionTabs = ({
104110
>
105111
{t('subscription-settings.widget.user-status-remarks')}
106112
</Tabs.Tab>
113+
<Tabs.Tab
114+
leftSection={
115+
<TbPrescription color={theme.colors.indigo[6]} size="1.3rem" />
116+
}
117+
value="additional-response-headers"
118+
>
119+
{t('subscription-tabs.widget.additional-response-headers')}
120+
</Tabs.Tab>
107121
</Tabs.List>
108122

109123
<Tabs.Panel value="general">
@@ -470,6 +484,36 @@ export const SubscriptionTabs = ({
470484
</Card.Section>
471485
</Card>
472486
</Tabs.Panel>
487+
488+
<Tabs.Panel value="additional-response-headers">
489+
<Card mt="sm" p="md" radius="md" shadow="sm" withBorder>
490+
<Card.Section inheritPadding p="md" withBorder>
491+
<Group align="flex-start" wrap="nowrap">
492+
<ThemeIcon color="indigo" radius="md" size="lg" variant="light">
493+
<TbPrescription size="1.5rem" />
494+
</ThemeIcon>
495+
496+
<Stack gap="xs">
497+
<Title fw={600} order={4}>
498+
{t(
499+
'subscription-tabs.widget.additional-response-headers'
500+
)}
501+
</Title>
502+
503+
<Text c="dimmed" size="sm">
504+
{t(
505+
'subscription-tabs.widget.headers-that-will-be-sent-with-subscription-content'
506+
)}
507+
</Text>
508+
</Stack>
509+
</Group>
510+
</Card.Section>
511+
512+
<Card.Section p="md" pt="xl">
513+
<HeadersManager initialHeaders={headers} onChange={updateHeaders} />
514+
</Card.Section>
515+
</Card>
516+
</Tabs.Panel>
473517
</Tabs>
474518

475519
<Group justify="flex-start" mb="xl">

0 commit comments

Comments
 (0)