Merge branch 'dev' into feat/internal-user-passkey-support

This commit is contained in:
Adrian Astles
2025-07-05 18:36:44 +08:00
committed by GitHub
8 changed files with 470 additions and 383 deletions

49
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Run Tests
on:
pull_request:
branches:
- main
- dev
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Copy config file
run: cp config/config.example.yml config/config.yml
- name: Install dependencies
run: npm ci
- name: Generate database migrations
run: npm run db:sqlite:generate
- name: Apply database migrations
run: npm run db:sqlite:push
- name: Start app in background
run: nohup npm run dev &
- name: Wait for app availability
run: |
for i in {1..5}; do
if curl --silent --fail http://localhost:3002/auth/login; then
echo "App is up"
exit 0
fi
echo "Waiting for the app... attempt $i"
sleep 5
done
echo "App failed to start"
exit 1
- name: Build Docker image
run: make build

View File

@@ -3,8 +3,8 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# COPY package.json package-lock.json ./ # COPY package.json package-lock.json ./
COPY package.json ./ COPY package*.json ./
RUN npm install RUN npm ci
COPY . . COPY . .
@@ -23,8 +23,8 @@ WORKDIR /app
RUN apk add --no-cache curl RUN apk add --no-cache curl
# COPY package.json package-lock.json ./ # COPY package.json package-lock.json ./
COPY package.json ./ COPY package*.json ./
RUN npm install --only=production && npm cache clean --force RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static

View File

@@ -3,8 +3,8 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# COPY package.json package-lock.json ./ # COPY package.json package-lock.json ./
COPY package.json ./ COPY package*.json ./
RUN npm install RUN npm ci
COPY . . COPY . .
@@ -23,8 +23,8 @@ WORKDIR /app
RUN apk add --no-cache curl RUN apk add --no-cache curl
# COPY package.json package-lock.json ./ # COPY package.json package-lock.json ./
COPY package.json ./ COPY package*.json ./
RUN npm install --only=production && npm cache clean --force RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static

View File

@@ -1,6 +1,8 @@
.PHONY: build build-release build-arm build-x86 test clean
build-release: build-release:
@if [ -z "$(tag)" ]; then \ @if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \ echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \ exit 1; \
fi fi
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push . docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push .

577
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.4", "@asteasolutions/zod-to-openapi": "^7.3.4",
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "3.9.1",
"@node-rs/argon2": "^2.0.2", "@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
@@ -49,7 +49,7 @@
"@radix-ui/react-switch": "1.2.5", "@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "1.1.12", "@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toast": "1.2.14", "@radix-ui/react-toast": "1.2.14",
"@react-email/components": "0.0.41", "@react-email/components": "0.1.0",
"@react-email/render": "^1.1.2", "@react-email/render": "^1.1.2",
"@react-email/tailwind": "1.0.5", "@react-email/tailwind": "1.0.5",
"@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/browser": "^13.1.0",
@@ -57,7 +57,7 @@
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0", "arctic": "^3.7.0",
"axios": "1.9.0", "axios": "1.10.0",
"better-sqlite3": "11.7.0", "better-sqlite3": "11.7.0",
"canvas-confetti": "1.9.3", "canvas-confetti": "1.9.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -68,12 +68,12 @@
"cookies": "^0.9.1", "cookies": "^0.9.1",
"cors": "2.8.5", "cors": "2.8.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"drizzle-orm": "0.38.3", "drizzle-orm": "0.44.2",
"eslint": "9.28.0", "eslint": "9.29.0",
"eslint-config-next": "15.3.3", "eslint-config-next": "15.3.4",
"express": "4.21.2", "express": "4.21.2",
"express-rate-limit": "7.5.0", "express-rate-limit": "7.5.1",
"glob": "11.0.2", "glob": "11.0.3",
"helmet": "8.1.0", "helmet": "8.1.0",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"i": "^0.3.7", "i": "^0.3.7",
@@ -81,41 +81,41 @@
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^0.525.0", "lucide-react": "0.522.0",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.3.3", "next": "15.3.4",
"next-intl": "^4.1.0", "next-intl": "^4.1.0",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.16", "nodemailer": "7.0.3",
"npm": "^11.4.1", "npm": "^11.4.2",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "^8.16.0", "pg": "^8.16.2",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-easy-sort": "^1.6.0", "react-easy-sort": "^1.6.0",
"react-hook-form": "^7.60.0", "react-hook-form": "7.58.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"rebuild": "0.1.2", "rebuild": "0.1.2",
"semver": "^7.7.2", "semver": "^7.7.2",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"tailwind-merge": "2.6.0", "tailwind-merge": "3.3.1",
"tw-animate-css": "^1.3.3", "tw-animate-css": "^1.3.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vaul": "1.1.2", "vaul": "1.1.2",
"winston": "3.17.0", "winston": "3.17.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.18.2", "ws": "8.18.2",
"yargs": "18.0.0", "zod": "3.25.67",
"zod": "^3.25.74", "zod-validation-error": "3.5.2",
"zod-validation-error": "3.4.1" "yargs": "18.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.44.1", "@dotenvx/dotenvx": "1.45.1",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@tailwindcss/postcss": "^4.1.8", "@tailwindcss/postcss": "^4.1.10",
"@types/better-sqlite3": "7.6.12", "@types/better-sqlite3": "7.6.12",
"@types/cookie-parser": "1.4.9", "@types/cookie-parser": "1.4.9",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
@@ -124,25 +124,25 @@
"@types/express-session": "^1.18.2", "@types/express-session": "^1.18.2",
"@types/jmespath": "^0.15.2", "@types/jmespath": "^0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22", "@types/node": "^24",
"@types/nodemailer": "6.4.17", "@types/nodemailer": "6.4.17",
"@types/react": "19.1.7", "@types/react": "19.1.8",
"@types/react-dom": "19.1.6", "@types/react-dom": "19.1.6",
"@types/semver": "^7.7.0", "@types/semver": "^7.7.0",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"drizzle-kit": "0.31.1", "drizzle-kit": "0.31.2",
"esbuild": "0.25.5", "esbuild": "0.25.5",
"esbuild-node-externals": "1.18.0", "esbuild-node-externals": "1.18.0",
"postcss": "^8", "postcss": "^8",
"react-email": "4.0.16", "react-email": "4.0.16",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsx": "4.19.4", "tsx": "4.20.3",
"typescript": "^5", "typescript": "^5",
"typescript-eslint": "^8.34.0" "typescript-eslint": "^8.35.0"
}, },
"overrides": { "overrides": {
"emblor": { "emblor": {

View File

@@ -4,7 +4,7 @@ import semver from "semver";
import { versionMigrations } from "../db/pg"; import { versionMigrations } from "../db/pg";
import { __DIRNAME, APP_VERSION } from "@server/lib/consts"; import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
import path from "path"; import path from "path";
import m1 from "./scriptsSqlite/1.6.0"; import m1 from "./scriptsPg/1.6.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA // EXCEPT FOR THE DATABASE AND THE SCHEMA

View File

@@ -128,7 +128,7 @@ export default function ReverseProxyTargets(props: {
return true; return true;
}, },
{ {
message: t('proxyErrorInvalidHeader') message: t("proxyErrorInvalidHeader")
} }
) )
}); });
@@ -146,7 +146,7 @@ export default function ReverseProxyTargets(props: {
return true; return true;
}, },
{ {
message: t('proxyErrorTls') message: t("proxyErrorTls")
} }
) )
}); });
@@ -203,10 +203,10 @@ export default function ReverseProxyTargets(props: {
console.error(err); console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('targetErrorFetch'), title: t("targetErrorFetch"),
description: formatAxiosError( description: formatAxiosError(
err, err,
t('targetErrorFetchDescription') t("targetErrorFetchDescription")
) )
}); });
} finally { } finally {
@@ -228,10 +228,10 @@ export default function ReverseProxyTargets(props: {
console.error(err); console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('siteErrorFetch'), title: t("siteErrorFetch"),
description: formatAxiosError( description: formatAxiosError(
err, err,
t('siteErrorFetchDescription') t("siteErrorFetchDescription")
) )
}); });
} }
@@ -251,8 +251,8 @@ export default function ReverseProxyTargets(props: {
if (isDuplicate) { if (isDuplicate) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('targetErrorDuplicate'), title: t("targetErrorDuplicate"),
description: t('targetErrorDuplicateDescription') description: t("targetErrorDuplicateDescription")
}); });
return; return;
} }
@@ -261,11 +261,23 @@ export default function ReverseProxyTargets(props: {
// make sure that the target IP is within the site subnet // make sure that the target IP is within the site subnet
const targetIp = data.ip; const targetIp = data.ip;
const subnet = site.subnet; const subnet = site.subnet;
if (!isIPInSubnet(targetIp, subnet)) { try {
if (!isIPInSubnet(targetIp, subnet)) {
toast({
variant: "destructive",
title: t("targetWireGuardErrorInvalidIp"),
description: t(
"targetWireGuardErrorInvalidIpDescription"
)
});
return;
}
} catch (error) {
console.error(error);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('targetWireGuardErrorInvalidIp'), title: t("targetWireGuardErrorInvalidIp"),
description: t('targetWireGuardErrorInvalidIpDescription') description: t("targetWireGuardErrorInvalidIpDescription")
}); });
return; return;
} }
@@ -343,8 +355,8 @@ export default function ReverseProxyTargets(props: {
updateResource({ stickySession: stickySessionData.stickySession }); updateResource({ stickySession: stickySessionData.stickySession });
toast({ toast({
title: t('targetsUpdated'), title: t("targetsUpdated"),
description: t('targetsUpdatedDescription') description: t("targetsUpdatedDescription")
}); });
setTargetsToRemove([]); setTargetsToRemove([]);
@@ -353,10 +365,10 @@ export default function ReverseProxyTargets(props: {
console.error(err); console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('targetsErrorUpdate'), title: t("targetsErrorUpdate"),
description: formatAxiosError( description: formatAxiosError(
err, err,
t('targetsErrorUpdateDescription') t("targetsErrorUpdateDescription")
) )
}); });
} finally { } finally {
@@ -377,17 +389,17 @@ export default function ReverseProxyTargets(props: {
tlsServerName: data.tlsServerName || null tlsServerName: data.tlsServerName || null
}); });
toast({ toast({
title: t('targetTlsUpdate'), title: t("targetTlsUpdate"),
description: t('targetTlsUpdateDescription') description: t("targetTlsUpdateDescription")
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('targetErrorTlsUpdate'), title: t("targetErrorTlsUpdate"),
description: formatAxiosError( description: formatAxiosError(
err, err,
t('targetErrorTlsUpdateDescription') t("targetErrorTlsUpdateDescription")
) )
}); });
} finally { } finally {
@@ -406,17 +418,17 @@ export default function ReverseProxyTargets(props: {
setHostHeader: data.setHostHeader || null setHostHeader: data.setHostHeader || null
}); });
toast({ toast({
title: t('proxyUpdated'), title: t("proxyUpdated"),
description: t('proxyUpdatedDescription') description: t("proxyUpdatedDescription")
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('proxyErrorUpdate'), title: t("proxyErrorUpdate"),
description: formatAxiosError( description: formatAxiosError(
err, err,
t('proxyErrorUpdateDescription') t("proxyErrorUpdateDescription")
) )
}); });
} finally { } finally {
@@ -427,7 +439,7 @@ export default function ReverseProxyTargets(props: {
const columns: ColumnDef<LocalTarget>[] = [ const columns: ColumnDef<LocalTarget>[] = [
{ {
accessorKey: "ip", accessorKey: "ip",
header: t('targetAddr'), header: t("targetAddr"),
cell: ({ row }) => ( cell: ({ row }) => (
<Input <Input
defaultValue={row.original.ip} defaultValue={row.original.ip}
@@ -442,7 +454,7 @@ export default function ReverseProxyTargets(props: {
}, },
{ {
accessorKey: "port", accessorKey: "port",
header: t('targetPort'), header: t("targetPort"),
cell: ({ row }) => ( cell: ({ row }) => (
<Input <Input
type="number" type="number"
@@ -476,7 +488,7 @@ export default function ReverseProxyTargets(props: {
// }, // },
{ {
accessorKey: "enabled", accessorKey: "enabled",
header: t('enabled'), header: t("enabled"),
cell: ({ row }) => ( cell: ({ row }) => (
<Switch <Switch
defaultChecked={row.original.enabled} defaultChecked={row.original.enabled}
@@ -503,7 +515,7 @@ export default function ReverseProxyTargets(props: {
variant="outline" variant="outline"
onClick={() => removeTarget(row.original.targetId)} onClick={() => removeTarget(row.original.targetId)}
> >
{t('delete')} {t("delete")}
</Button> </Button>
</div> </div>
</> </>
@@ -514,7 +526,7 @@ export default function ReverseProxyTargets(props: {
if (resource.http) { if (resource.http) {
const methodCol: ColumnDef<LocalTarget> = { const methodCol: ColumnDef<LocalTarget> = {
accessorKey: "method", accessorKey: "method",
header: t('method'), header: t("method"),
cell: ({ row }) => ( cell: ({ row }) => (
<Select <Select
defaultValue={row.original.method ?? ""} defaultValue={row.original.method ?? ""}
@@ -561,11 +573,9 @@ export default function ReverseProxyTargets(props: {
<SettingsContainer> <SettingsContainer>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>{t("targets")}</SettingsSectionTitle>
{t('targets')}
</SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('targetsDescription')} {t("targetsDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@@ -587,8 +597,12 @@ export default function ReverseProxyTargets(props: {
<FormControl> <FormControl>
<SwitchInput <SwitchInput
id="sticky-toggle" id="sticky-toggle"
label={t('targetStickySessions')} label={t(
description={t('targetStickySessionsDescription')} "targetStickySessions"
)}
description={t(
"targetStickySessionsDescription"
)}
defaultChecked={ defaultChecked={
field.value field.value
} }
@@ -619,7 +633,9 @@ export default function ReverseProxyTargets(props: {
name="method" name="method"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('method')}</FormLabel> <FormLabel>
{t("method")}
</FormLabel>
<FormControl> <FormControl>
<Select <Select
value={ value={
@@ -636,7 +652,11 @@ export default function ReverseProxyTargets(props: {
}} }}
> >
<SelectTrigger id="method"> <SelectTrigger id="method">
<SelectValue placeholder={t('methodSelect')} /> <SelectValue
placeholder={t(
"methodSelect"
)}
/>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="http"> <SelectItem value="http">
@@ -662,7 +682,9 @@ export default function ReverseProxyTargets(props: {
name="ip" name="ip"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative"> <FormItem className="relative">
<FormLabel>{t('targetAddr')}</FormLabel> <FormLabel>
{t("targetAddr")}
</FormLabel>
<FormControl> <FormControl>
<Input id="ip" {...field} /> <Input id="ip" {...field} />
</FormControl> </FormControl>
@@ -695,7 +717,9 @@ export default function ReverseProxyTargets(props: {
name="port" name="port"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('targetPort')}</FormLabel> <FormLabel>
{t("targetPort")}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
id="port" id="port"
@@ -714,7 +738,7 @@ export default function ReverseProxyTargets(props: {
className="mt-6" className="mt-6"
disabled={!(watchedIp && watchedPort)} disabled={!(watchedIp && watchedPort)}
> >
{t('targetSubmit')} {t("targetSubmit")}
</Button> </Button>
</div> </div>
</form> </form>
@@ -758,13 +782,13 @@ export default function ReverseProxyTargets(props: {
colSpan={columns.length} colSpan={columns.length}
className="h-24 text-center" className="h-24 text-center"
> >
{t('targetNoOne')} {t("targetNoOne")}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
</TableBody> </TableBody>
<TableCaption> <TableCaption>
{t('targetNoOneDescription')} {t("targetNoOneDescription")}
</TableCaption> </TableCaption>
</Table> </Table>
</SettingsSectionBody> </SettingsSectionBody>
@@ -775,7 +799,7 @@ export default function ReverseProxyTargets(props: {
disabled={targetsLoading} disabled={targetsLoading}
form="targets-settings-form" form="targets-settings-form"
> >
{t('targetsSubmit')} {t("targetsSubmit")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
@@ -785,10 +809,10 @@ export default function ReverseProxyTargets(props: {
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('targetTlsSettings')} {t("targetTlsSettings")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('targetTlsSettingsDescription')} {t("targetTlsSettingsDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@@ -809,7 +833,9 @@ export default function ReverseProxyTargets(props: {
<FormControl> <FormControl>
<SwitchInput <SwitchInput
id="ssl-toggle" id="ssl-toggle"
label={t('proxyEnableSSL')} label={t(
"proxyEnableSSL"
)}
defaultChecked={ defaultChecked={
field.value field.value
} }
@@ -838,7 +864,9 @@ export default function ReverseProxyTargets(props: {
className="p-0 flex items-center justify-start gap-2 w-full" className="p-0 flex items-center justify-start gap-2 w-full"
> >
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t('targetTlsSettingsAdvanced')} {t(
"targetTlsSettingsAdvanced"
)}
</p> </p>
<div> <div>
<ChevronsUpDown className="h-4 w-4" /> <ChevronsUpDown className="h-4 w-4" />
@@ -858,7 +886,9 @@ export default function ReverseProxyTargets(props: {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('targetTlsSni')} {t(
"targetTlsSni"
)}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@@ -866,7 +896,9 @@ export default function ReverseProxyTargets(props: {
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('targetTlsSniDescription')} {t(
"targetTlsSniDescription"
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -884,17 +916,17 @@ export default function ReverseProxyTargets(props: {
loading={httpsTlsLoading} loading={httpsTlsLoading}
form="tls-settings-form" form="tls-settings-form"
> >
{t('targetTlsSubmit')} {t("targetTlsSubmit")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('proxyAdditional')} {t("proxyAdditional")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('proxyAdditionalDescription')} {t("proxyAdditionalDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@@ -913,13 +945,15 @@ export default function ReverseProxyTargets(props: {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('proxyCustomHeader')} {t("proxyCustomHeader")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('proxyCustomHeaderDescription')} {t(
"proxyCustomHeaderDescription"
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -935,7 +969,7 @@ export default function ReverseProxyTargets(props: {
loading={proxySettingsLoading} loading={proxySettingsLoading}
form="proxy-settings-form" form="proxy-settings-form"
> >
{t('targetTlsSubmit')} {t("targetTlsSubmit")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
@@ -950,10 +984,8 @@ function isIPInSubnet(subnet: string, ip: string): boolean {
const [subnetIP, maskBits] = subnet.split("/"); const [subnetIP, maskBits] = subnet.split("/");
const mask = parseInt(maskBits); const mask = parseInt(maskBits);
const t = useTranslations();
if (mask < 0 || mask > 32) { if (mask < 0 || mask > 32) {
throw new Error(t('subnetMaskErrorInvalid')); throw new Error("subnetMaskErrorInvalid");
} }
// Convert IP addresses to binary numbers // Convert IP addresses to binary numbers
@@ -970,17 +1002,16 @@ function isIPInSubnet(subnet: string, ip: string): boolean {
function ipToNumber(ip: string): number { function ipToNumber(ip: string): number {
// Validate IP address format // Validate IP address format
const parts = ip.split("."); const parts = ip.split(".");
const t = useTranslations();
if (parts.length !== 4) { if (parts.length !== 4) {
throw new Error(t('ipAddressErrorInvalidFormat')); throw new Error("ipAddressErrorInvalidFormat");
} }
// Convert IP octets to 32-bit number // Convert IP octets to 32-bit number
return parts.reduce((num, octet) => { return parts.reduce((num, octet) => {
const oct = parseInt(octet); const oct = parseInt(octet);
if (isNaN(oct) || oct < 0 || oct > 255) { if (isNaN(oct) || oct < 0 || oct > 255) {
throw new Error(t('ipAddressErrorInvalidOctet')); throw new Error("ipAddressErrorInvalidOctet");
} }
return (num << 8) + oct; return (num << 8) + oct;
}, 0); }, 0);