mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Merge branch 'feat/add-proxy-protocol-support' into dev
This commit is contained in:
@@ -51,3 +51,12 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://pangolin:3000" # API/WebSocket server
|
- url: "http://pangolin:3000" # API/WebSocket server
|
||||||
|
|
||||||
|
tcp:
|
||||||
|
serversTransports:
|
||||||
|
pp-transport-v1:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 1
|
||||||
|
pp-transport-v2:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 2
|
||||||
@@ -1911,5 +1911,15 @@
|
|||||||
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
|
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
|
||||||
"loadingDNSRecords": "Loading DNS records...",
|
"loadingDNSRecords": "Loading DNS records...",
|
||||||
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
|
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
|
||||||
"client": "Client"
|
"client": "Client",
|
||||||
|
"proxyProtocol": "Proxy Protocol Settings",
|
||||||
|
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP/UDP services.",
|
||||||
|
"enableProxyProtocol": "Enable Proxy Protocol",
|
||||||
|
"proxyProtocolInfo": "Preserve client IP addresses for TCP/UDP backends",
|
||||||
|
"proxyProtocolVersion": "Proxy Protocol Version",
|
||||||
|
"version1": " Version 1 (Recommended)",
|
||||||
|
"version2": "Version 2",
|
||||||
|
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible.",
|
||||||
|
"warning": "Warning",
|
||||||
|
"proxyProtocolWarning": "Your backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections. Make sure to configure your backend to trust Proxy Protocol headers from Traefik."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ export const resources = pgTable("resources", {
|
|||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
headers: text("headers") // comma-separated list of headers to add to the request
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
|
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||||
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
|
|||||||
@@ -128,7 +128,10 @@ export const resources = sqliteTable("resources", {
|
|||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
headers: text("headers") // comma-separated list of headers to add to the request
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
|
proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false),
|
||||||
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
|
|||||||
@@ -309,10 +309,7 @@ export class TraefikConfigManager {
|
|||||||
this.lastActiveDomains = new Set(domains);
|
this.lastActiveDomains = new Set(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (process.env.USE_PANGOLIN_DNS === "true" && build != "oss") {
|
||||||
process.env.USE_PANGOLIN_DNS === "true" &&
|
|
||||||
build != "oss"
|
|
||||||
) {
|
|
||||||
// Scan current local certificate state
|
// Scan current local certificate state
|
||||||
this.lastLocalCertificateState =
|
this.lastLocalCertificateState =
|
||||||
await this.scanLocalCertificateState();
|
await this.scanLocalCertificateState();
|
||||||
@@ -450,7 +447,8 @@ export class TraefikConfigManager {
|
|||||||
currentExitNode,
|
currentExitNode,
|
||||||
config.getRawConfig().traefik.site_types,
|
config.getRawConfig().traefik.site_types,
|
||||||
build == "oss", // filter out the namespace domains in open source
|
build == "oss", // filter out the namespace domains in open source
|
||||||
build != "oss" // generate the login pages on the cloud and hybrid
|
build != "oss", // generate the login pages on the cloud and hybrid,
|
||||||
|
build == "saas" ? false : config.getRawConfig().traefik.allow_raw_resources // dont allow raw resources on saas otherwise use config
|
||||||
);
|
);
|
||||||
|
|
||||||
const domains = new Set<string>();
|
const domains = new Set<string>();
|
||||||
@@ -502,6 +500,25 @@ export class TraefikConfigManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tcp:
|
||||||
|
// serversTransports:
|
||||||
|
// pp-transport-v1:
|
||||||
|
// proxyProtocol:
|
||||||
|
// version: 1
|
||||||
|
// pp-transport-v2:
|
||||||
|
// proxyProtocol:
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
if (build != "saas") {
|
||||||
|
// add the serversTransports section if not present
|
||||||
|
if (traefikConfig.tcp && !traefikConfig.tcp.serversTransports) {
|
||||||
|
traefikConfig.tcp.serversTransports = {
|
||||||
|
"pp-transport-v1": { proxyProtocol: { version: 1 } },
|
||||||
|
"pp-transport-v2": { proxyProtocol: { version: 2 } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { domains, traefikConfig };
|
return { domains, traefikConfig };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// pull data out of the axios error to log
|
// pull data out of the axios error to log
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export async function getTraefikConfig(
|
|||||||
exitNodeId: number,
|
exitNodeId: number,
|
||||||
siteTypes: string[],
|
siteTypes: string[],
|
||||||
filterOutNamespaceDomains = false,
|
filterOutNamespaceDomains = false,
|
||||||
generateLoginPageRouters = false
|
generateLoginPageRouters = false,
|
||||||
|
allowRawResources = true
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// Define extended target type with site information
|
// Define extended target type with site information
|
||||||
type TargetWithSite = Target & {
|
type TargetWithSite = Target & {
|
||||||
@@ -56,6 +57,8 @@ export async function getTraefikConfig(
|
|||||||
setHostHeader: resources.setHostHeader,
|
setHostHeader: resources.setHostHeader,
|
||||||
enableProxy: resources.enableProxy,
|
enableProxy: resources.enableProxy,
|
||||||
headers: resources.headers,
|
headers: resources.headers,
|
||||||
|
proxyProtocol: resources.proxyProtocol,
|
||||||
|
proxyProtocolVersion: resources.proxyProtocolVersion,
|
||||||
// Target fields
|
// Target fields
|
||||||
targetId: targets.targetId,
|
targetId: targets.targetId,
|
||||||
targetEnabled: targets.enabled,
|
targetEnabled: targets.enabled,
|
||||||
@@ -104,7 +107,7 @@ export async function getTraefikConfig(
|
|||||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||||
),
|
),
|
||||||
inArray(sites.type, siteTypes),
|
inArray(sites.type, siteTypes),
|
||||||
config.getRawConfig().traefik.allow_raw_resources
|
allowRawResources
|
||||||
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
||||||
: eq(resources.http, true)
|
: eq(resources.http, true)
|
||||||
)
|
)
|
||||||
@@ -167,6 +170,8 @@ export async function getTraefikConfig(
|
|||||||
enableProxy: row.enableProxy,
|
enableProxy: row.enableProxy,
|
||||||
targets: [],
|
targets: [],
|
||||||
headers: row.headers,
|
headers: row.headers,
|
||||||
|
proxyProtocol: row.proxyProtocol,
|
||||||
|
proxyProtocolVersion: row.proxyProtocolVersion ?? 1,
|
||||||
path: row.path, // the targets will all have the same path
|
path: row.path, // the targets will all have the same path
|
||||||
pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType
|
pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType
|
||||||
rewritePath: row.rewritePath,
|
rewritePath: row.rewritePath,
|
||||||
@@ -635,6 +640,11 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
})(),
|
})(),
|
||||||
|
...(resource.proxyProtocol && protocol == "tcp"
|
||||||
|
? {
|
||||||
|
serversTransport: `pp-transport-v${resource.proxyProtocolVersion || 1}`
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ export async function getTraefikConfig(
|
|||||||
exitNodeId: number,
|
exitNodeId: number,
|
||||||
siteTypes: string[],
|
siteTypes: string[],
|
||||||
filterOutNamespaceDomains = false,
|
filterOutNamespaceDomains = false,
|
||||||
generateLoginPageRouters = false
|
generateLoginPageRouters = false,
|
||||||
|
allowRawResources = true
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// Define extended target type with site information
|
// Define extended target type with site information
|
||||||
type TargetWithSite = Target & {
|
type TargetWithSite = Target & {
|
||||||
@@ -141,7 +142,7 @@ export async function getTraefikConfig(
|
|||||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||||
),
|
),
|
||||||
inArray(sites.type, siteTypes),
|
inArray(sites.type, siteTypes),
|
||||||
config.getRawConfig().traefik.allow_raw_resources
|
allowRawResources
|
||||||
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
||||||
: eq(resources.http, true)
|
: eq(resources.http, true)
|
||||||
)
|
)
|
||||||
@@ -709,6 +710,11 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
})(),
|
})(),
|
||||||
|
...(resource.proxyProtocol && protocol == "tcp" // proxy protocol only works for tcp
|
||||||
|
? {
|
||||||
|
serversTransport: `pp-transport-v${resource.proxyProtocolVersion || 1}`
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
|
|||||||
@@ -270,7 +270,8 @@ hybridRouter.get(
|
|||||||
remoteExitNode.exitNodeId,
|
remoteExitNode.exitNodeId,
|
||||||
["newt", "local", "wireguard"], // Allow them to use all the site types
|
["newt", "local", "wireguard"], // Allow them to use all the site types
|
||||||
true, // But don't allow domain namespace resources
|
true, // But don't allow domain namespace resources
|
||||||
false // Dont include login pages
|
false, // Dont include login pages,
|
||||||
|
true // allow raw resources
|
||||||
);
|
);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -99,8 +99,9 @@ const updateRawResourceBodySchema = z
|
|||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||||
stickySession: z.boolean().optional(),
|
stickySession: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional()
|
enabled: z.boolean().optional(),
|
||||||
// enableProxy: z.boolean().optional() // always true now
|
proxyProtocol: z.boolean().optional(),
|
||||||
|
proxyProtocolVersion: z.number().int().min(1).optional()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export async function traefikConfigProvider(
|
|||||||
currentExitNodeId,
|
currentExitNodeId,
|
||||||
config.getRawConfig().traefik.site_types,
|
config.getRawConfig().traefik.site_types,
|
||||||
build == "oss", // filter out the namespace domains in open source
|
build == "oss", // filter out the namespace domains in open source
|
||||||
build != "oss" // generate the login pages on the cloud and hybrid
|
build != "oss", // generate the login pages on the cloud and and enterprise,
|
||||||
|
config.getRawConfig().traefik.allow_raw_resources
|
||||||
);
|
);
|
||||||
|
|
||||||
if (traefikConfig?.http?.middlewares) {
|
if (traefikConfig?.http?.middlewares) {
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ import {
|
|||||||
MoveRight,
|
MoveRight,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
Info,
|
Info,
|
||||||
ArrowDown
|
ArrowDown,
|
||||||
|
AlertTriangle
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { ContainersSelector } from "@app/components/ContainersSelector";
|
import { ContainersSelector } from "@app/components/ContainersSelector";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -115,6 +116,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@app/components/ui/tooltip";
|
} from "@app/components/ui/tooltip";
|
||||||
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
|
|
||||||
const addTargetSchema = z
|
const addTargetSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -288,7 +290,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
),
|
),
|
||||||
headers: z
|
headers: z
|
||||||
.array(z.object({ name: z.string(), value: z.string() }))
|
.array(z.object({ name: z.string(), value: z.string() }))
|
||||||
.nullable()
|
.nullable(),
|
||||||
|
proxyProtocol: z.boolean().optional(),
|
||||||
|
proxyProtocolVersion: z.number().int().min(1).max(2).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const tlsSettingsSchema = z.object({
|
const tlsSettingsSchema = z.object({
|
||||||
@@ -325,7 +329,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
resolver: zodResolver(proxySettingsSchema),
|
resolver: zodResolver(proxySettingsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
setHostHeader: resource.setHostHeader || "",
|
setHostHeader: resource.setHostHeader || "",
|
||||||
headers: resource.headers
|
headers: resource.headers,
|
||||||
|
proxyProtocol: resource.proxyProtocol || false,
|
||||||
|
proxyProtocolVersion: resource.proxyProtocolVersion || 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -549,11 +555,11 @@ export default function ReverseProxyTargets(props: {
|
|||||||
prev.map((t) =>
|
prev.map((t) =>
|
||||||
t.targetId === target.targetId
|
t.targetId === target.targetId
|
||||||
? {
|
? {
|
||||||
...t,
|
...t,
|
||||||
targetId: response.data.data.targetId,
|
targetId: response.data.data.targetId,
|
||||||
new: false,
|
new: false,
|
||||||
updated: false
|
updated: false
|
||||||
}
|
}
|
||||||
: t
|
: t
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -673,11 +679,11 @@ export default function ReverseProxyTargets(props: {
|
|||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...data,
|
...data,
|
||||||
updated: true,
|
updated: true,
|
||||||
siteType: site ? site.type : target.siteType
|
siteType: site ? site.type : target.siteType
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -688,10 +694,10 @@ export default function ReverseProxyTargets(props: {
|
|||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...config,
|
...config,
|
||||||
updated: true
|
updated: true
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -800,6 +806,22 @@ export default function ReverseProxyTargets(props: {
|
|||||||
setHostHeader: proxyData.setHostHeader || null,
|
setHostHeader: proxyData.setHostHeader || null,
|
||||||
headers: proxyData.headers || null
|
headers: proxyData.headers || null
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// For TCP/UDP resources, save proxy protocol settings
|
||||||
|
const proxyData = proxySettingsForm.getValues();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
proxyProtocol: proxyData.proxyProtocol || false,
|
||||||
|
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.post(`/resource/${resource.resourceId}`, payload);
|
||||||
|
|
||||||
|
updateResource({
|
||||||
|
...resource,
|
||||||
|
proxyProtocol: proxyData.proxyProtocol || false,
|
||||||
|
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -1064,7 +1086,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||||
!row.original.siteId &&
|
!row.original.siteId &&
|
||||||
"text-muted-foreground"
|
"text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate max-w-[150px]">
|
<span className="truncate max-w-[150px]">
|
||||||
@@ -1404,12 +1426,12 @@ export default function ReverseProxyTargets(props: {
|
|||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header
|
header
|
||||||
.column
|
.column
|
||||||
.columnDef
|
.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -1675,6 +1697,102 @@ export default function ReverseProxyTargets(props: {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!resource.http && resource.protocol && (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("proxyProtocol")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("proxyProtocolDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...proxySettingsForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={proxySettingsForm.handleSubmit(
|
||||||
|
saveAllSettings
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="proxy-protocol-settings-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={proxySettingsForm.control}
|
||||||
|
name="proxyProtocol"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<SwitchInput
|
||||||
|
id="proxy-protocol-toggle"
|
||||||
|
label={t(
|
||||||
|
"enableProxyProtocol"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"proxyProtocolInfo"
|
||||||
|
)}
|
||||||
|
defaultChecked={
|
||||||
|
field.value || false
|
||||||
|
}
|
||||||
|
onCheckedChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{proxySettingsForm.watch("proxyProtocol") && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={proxySettingsForm.control}
|
||||||
|
name="proxyProtocolVersion"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("proxyProtocolVersion")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
value={String(field.value || 1)}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
field.onChange(parseInt(value, 10))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select version" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">
|
||||||
|
{t("version1")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="2">
|
||||||
|
{t("version2")}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t("versionDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<strong>{t("warning")}:</strong> {t("proxyProtocolWarning")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end mt-6">
|
<div className="flex justify-end mt-6">
|
||||||
<Button
|
<Button
|
||||||
onClick={saveAllSettings}
|
onClick={saveAllSettings}
|
||||||
|
|||||||
Reference in New Issue
Block a user