From df31c1391256fbc5845a28f69cd1170ee95dd7da Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:59:25 +0800 Subject: [PATCH 001/219] added real-time password validation to signup form. --- messages/en-US.json | 20 ++- src/app/auth/signup/SignupForm.tsx | 209 ++++++++++++++++++++++++++--- 2 files changed, 212 insertions(+), 17 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index ed004d99..cd31f7f9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -834,6 +834,24 @@ "pincodeRequirementsLength": "PIN must be exactly 6 digits", "pincodeRequirementsChars": "PIN must only contain numbers", "passwordRequirementsLength": "Password must be at least 1 character long", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP must be at least 1 character long", "otpEmailSent": "OTP Sent", "otpEmailSentDescription": "An OTP has been sent to your email", @@ -1281,4 +1299,4 @@ "and": "and", "privacyPolicy": "privacy policy" } -} +} \ No newline at end of file diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index 5494ba10..d6d79eb7 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; @@ -23,6 +23,7 @@ import { CardTitle } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Progress } from "@/components/ui/progress"; import { SignUpResponse } from "@server/routers/auth"; import { useRouter } from "next/navigation"; import { passwordSchema } from "@server/auth/passwordSchema"; @@ -35,6 +36,40 @@ import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; import BrandingLogo from "@app/components/BrandingLogo"; import { build } from "@server/build"; +import { Check, X } from "lucide-react"; +import { cn } from "@app/lib/cn"; + +// Password strength calculation +const calculatePasswordStrength = (password: string) => { + const requirements = { + length: password.length >= 8, + uppercase: /[A-Z]/.test(password), + lowercase: /[a-z]/.test(password), + number: /[0-9]/.test(password), + special: /[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]/.test(password) + }; + + const score = Object.values(requirements).filter(Boolean).length; + let strength: "weak" | "medium" | "strong" = "weak"; + let color = "bg-red-500"; + let percentage = 0; + + if (score >= 5) { + strength = "strong"; + color = "bg-green-500"; + percentage = 100; + } else if (score >= 3) { + strength = "medium"; + color = "bg-yellow-500"; + percentage = 60; + } else if (score >= 1) { + strength = "weak"; + color = "bg-red-500"; + percentage = 30; + } + + return { requirements, strength, color, percentage, score }; +}; type SignupFormProps = { redirect?: string; @@ -71,14 +106,14 @@ export default function SignupForm({ inviteToken }: SignupFormProps) { const router = useRouter(); - - const { env } = useEnvContext(); - - const api = createApiClient({ env }); + const api = createApiClient(useEnvContext()); + const t = useTranslations(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [termsAgreedAt, setTermsAgreedAt] = useState(null); + const [passwordValue, setPasswordValue] = useState(""); + const [confirmPasswordValue, setConfirmPasswordValue] = useState(""); const form = useForm>({ resolver: zodResolver(formSchema), @@ -87,10 +122,12 @@ export default function SignupForm({ password: "", confirmPassword: "", agreeToTerms: false - } + }, + mode: "onChange" // Enable real-time validation }); - const t = useTranslations(); + const passwordStrength = calculatePasswordStrength(passwordValue); + const doPasswordsMatch = passwordValue.length > 0 && confirmPasswordValue.length > 0 && passwordValue === confirmPasswordValue; async function onSubmit(values: z.infer) { const { email, password } = values; @@ -183,11 +220,128 @@ export default function SignupForm({ name="password" render={({ field }) => ( - {t("password")} +
+ {t("password")} + {passwordStrength.strength === "strong" && ( + + )} +
- +
+ { + field.onChange(e); + setPasswordValue(e.target.value); + }} + className={cn( + passwordStrength.strength === "strong" && "border-green-500 focus-visible:ring-green-500", + passwordStrength.strength === "medium" && "border-yellow-500 focus-visible:ring-yellow-500", + passwordStrength.strength === "weak" && passwordValue.length > 0 && "border-red-500 focus-visible:ring-red-500" + )} + autoComplete="new-password" + /> +
- + + {passwordValue.length > 0 && ( +
+ {/* Password Strength Meter */} +
+
+ {t("passwordStrength")} + + {t(`passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}`)} + +
+ +
+ + {/* Requirements Checklist */} +
+
{t("passwordRequirements")}
+
+
+ {passwordStrength.requirements.length ? ( + + ) : ( + + )} + + {t("passwordRequirementLengthText")} + +
+
+ {passwordStrength.requirements.uppercase ? ( + + ) : ( + + )} + + {t("passwordRequirementUppercaseText")} + +
+
+ {passwordStrength.requirements.lowercase ? ( + + ) : ( + + )} + + {t("passwordRequirementLowercaseText")} + +
+
+ {passwordStrength.requirements.number ? ( + + ) : ( + + )} + + {t("passwordRequirementNumberText")} + +
+
+ {passwordStrength.requirements.special ? ( + + ) : ( + + )} + + {t("passwordRequirementSpecialText")} + +
+
+
+
+ )} + + {/* Only show FormMessage when not showing our custom requirements */} + {passwordValue.length === 0 && }
)} /> @@ -196,13 +350,36 @@ export default function SignupForm({ name="confirmPassword" render={({ field }) => ( - - {t("confirmPassword")} - +
+ {t('confirmPassword')} + {doPasswordsMatch && ( + + )} +
- +
+ { + field.onChange(e); + setConfirmPasswordValue(e.target.value); + }} + className={cn( + doPasswordsMatch && "border-green-500 focus-visible:ring-green-500", + confirmPasswordValue.length > 0 && !doPasswordsMatch && "border-red-500 focus-visible:ring-red-500" + )} + autoComplete="new-password" + /> +
- + {confirmPasswordValue.length > 0 && !doPasswordsMatch && ( +

+ {t("passwordsDoNotMatch")} +

+ )} + {/* Only show FormMessage when field is empty */} + {confirmPasswordValue.length === 0 && }
)} /> @@ -269,4 +446,4 @@ export default function SignupForm({ ); -} +} \ No newline at end of file From 350485612ee15c7dc49dc0f62bc5e7f740c461ed Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Fri, 25 Jul 2025 22:46:40 +0800 Subject: [PATCH 002/219] This improves the user experience by automatically filling the email field and preventing users from changing the email they were invited with. - Update invite link generation to include email parameter in URL - Modify signup form to pre-fill and lock email field when provided via invite - Update invite page and status card to preserve email through redirect chain - Ensure existing invite URLs continue to work without breaking changes --- server/routers/user/inviteUser.ts | 4 ++-- src/app/auth/signup/SignupForm.tsx | 11 ++++++++--- src/app/auth/signup/page.tsx | 6 +++++- src/app/invite/InviteStatusCard.tsx | 12 ++++++++++-- src/app/invite/page.tsx | 13 ++++++++++--- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 837ef179..174600fc 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -189,7 +189,7 @@ export async function inviteUser( ) ); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; if (doEmail) { await sendEmail( @@ -241,7 +241,7 @@ export async function inviteUser( }); }); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; if (doEmail) { await sendEmail( diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index d6d79eb7..f4690683 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -75,6 +75,7 @@ type SignupFormProps = { redirect?: string; inviteId?: string; inviteToken?: string; + emailParam?: string; }; const formSchema = z @@ -103,7 +104,8 @@ const formSchema = z export default function SignupForm({ redirect, inviteId, - inviteToken + inviteToken, + emailParam }: SignupFormProps) { const router = useRouter(); const api = createApiClient(useEnvContext()); @@ -118,7 +120,7 @@ export default function SignupForm({ const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - email: "", + email: emailParam || "", password: "", confirmPassword: "", agreeToTerms: false @@ -209,7 +211,10 @@ export default function SignupForm({ {t("email")} - + diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index debd7c58..673e69bf 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -11,7 +11,10 @@ import { getTranslations } from "next-intl/server"; export const dynamic = "force-dynamic"; export default async function Page(props: { - searchParams: Promise<{ redirect: string | undefined }>; + searchParams: Promise<{ + redirect: string | undefined; + email: string | undefined; + }>; }) { const searchParams = await props.searchParams; const getUser = cache(verifySession); @@ -69,6 +72,7 @@ export default async function Page(props: { redirect={redirectUrl} inviteToken={inviteToken} inviteId={inviteId} + emailParam={searchParams.email} />

diff --git a/src/app/invite/InviteStatusCard.tsx b/src/app/invite/InviteStatusCard.tsx index 3ecf16f5..6d7db4dc 100644 --- a/src/app/invite/InviteStatusCard.tsx +++ b/src/app/invite/InviteStatusCard.tsx @@ -17,11 +17,13 @@ import { useTranslations } from "next-intl"; type InviteStatusCardProps = { type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in"; token: string; + email?: string; }; export default function InviteStatusCard({ type, token, + email, }: InviteStatusCardProps) { const router = useRouter(); const api = createApiClient(useEnvContext()); @@ -29,12 +31,18 @@ export default function InviteStatusCard({ async function goToLogin() { await api.post("/auth/logout", {}); - router.push(`/auth/login?redirect=/invite?token=${token}`); + const redirectUrl = email + ? `/auth/login?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` + : `/auth/login?redirect=/invite?token=${token}`; + router.push(redirectUrl); } async function goToSignup() { await api.post("/auth/logout", {}); - router.push(`/auth/signup?redirect=/invite?token=${token}`); + const redirectUrl = email + ? `/auth/signup?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=/invite?token=${token}`; + router.push(redirectUrl); } function renderBody() { diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 014fb45b..0df7b810 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -14,6 +14,7 @@ export default async function InvitePage(props: { const params = await props.searchParams; const tokenParam = params.token as string; + const emailParam = params.email as string; if (!tokenParam) { redirect("/"); @@ -70,16 +71,22 @@ export default async function InvitePage(props: { const type = cardType(); if (!user && type === "user_does_not_exist") { - redirect(`/auth/signup?redirect=/invite?token=${params.token}`); + const redirectUrl = emailParam + ? `/auth/signup?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` + : `/auth/signup?redirect=/invite?token=${params.token}`; + redirect(redirectUrl); } if (!user && type === "not_logged_in") { - redirect(`/auth/login?redirect=/invite?token=${params.token}`); + const redirectUrl = emailParam + ? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` + : `/auth/login?redirect=/invite?token=${params.token}`; + redirect(redirectUrl); } return ( <> - + ); } From 39c43c0c0981bf08a7203a6f6b9fb992be82e022 Mon Sep 17 00:00:00 2001 From: Marvin <127591405+Lokowitz@users.noreply.github.com> Date: Sat, 26 Jul 2025 14:17:55 +0000 Subject: [PATCH 003/219] modified: .github/workflows/cicd.yml modified: .github/workflows/linting.yml modified: .github/workflows/test.yml modified: .nvmrc modified: Dockerfile.dev modified: Dockerfile.pg modified: Dockerfile.sqlite modified: esbuild.mjs modified: package-lock.json modified: tsconfig.json --- .github/workflows/cicd.yml | 2 +- .github/workflows/linting.yml | 2 +- .github/workflows/test.yml | 2 +- .nvmrc | 2 +- Dockerfile.dev | 2 +- Dockerfile.pg | 4 +- Dockerfile.sqlite | 4 +- esbuild.mjs | 2 +- package-lock.json | 2705 +-------------------------------- tsconfig.json | 2 +- 10 files changed, 20 insertions(+), 2707 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index c21b8985..5be89da3 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -30,7 +30,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.23.0 + go-version: 1.24 - name: Update version in package.json run: | diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 750f9ecc..4cc8ea8d 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Install dependencies run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 426e90bc..b5f11daf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Copy config file run: cp config/config.example.yml config/config.yml diff --git a/.nvmrc b/.nvmrc index 209e3ef4..2bd5a0a9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/Dockerfile.dev b/Dockerfile.dev index 141cfd10..c40775c2 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:22-alpine WORKDIR /app diff --git a/Dockerfile.pg b/Dockerfile.pg index f59a3153..8e45068d 100644 --- a/Dockerfile.pg +++ b/Dockerfile.pg @@ -1,4 +1,4 @@ -FROM node:20-alpine AS builder +FROM node:22-alpine AS builder WORKDIR /app @@ -15,7 +15,7 @@ RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema RUN npm run build:pg RUN npm run build:cli -FROM node:20-alpine AS runner +FROM node:22-alpine AS runner WORKDIR /app diff --git a/Dockerfile.sqlite b/Dockerfile.sqlite index e1c6e9b8..6a24a4af 100644 --- a/Dockerfile.sqlite +++ b/Dockerfile.sqlite @@ -1,4 +1,4 @@ -FROM node:20-alpine AS builder +FROM node:22-alpine AS builder WORKDIR /app @@ -15,7 +15,7 @@ RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema RUN npm run build:sqlite RUN npm run build:cli -FROM node:20-alpine AS runner +FROM node:22-alpine AS runner WORKDIR /app diff --git a/esbuild.mjs b/esbuild.mjs index 48a2fb31..b0227099 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -64,7 +64,7 @@ esbuild }), ], sourcemap: true, - target: "node20", + target: "node22", }) .then(() => { console.log("Build completed successfully"); diff --git a/package-lock.json b/package-lock.json index baec0b2b..e98069d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,6 @@ "http-errors": "2.0.0", "i": "^0.3.7", "input-otp": "1.4.2", - "ioredis": "^5.6.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", @@ -77,7 +76,6 @@ "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", - "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -362,37 +360,6 @@ "@noble/ciphers": "^1.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", - "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", - "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", - "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -405,261 +372,6 @@ "source-map-support": "^0.5.21" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", @@ -677,108 +389,6 @@ "node": ">=12" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -845,448 +455,6 @@ "typescript": "*" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1592,430 +760,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "license": "MIT" - }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2058,6 +802,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -2111,18 +856,6 @@ "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", "license": "MIT" }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, "node_modules/@next/env": { "version": "15.3.5", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.5.tgz", @@ -2145,7 +878,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -2161,7 +893,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -2177,7 +908,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2193,7 +923,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2241,7 +970,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2257,7 +985,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2333,134 +1060,6 @@ "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, - "node_modules/@node-rs/argon2-android-arm-eabi": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", - "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-android-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", - "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-darwin-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", - "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-darwin-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", - "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-freebsd-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", - "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", - "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-arm64-gnu": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", - "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-arm64-musl": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", - "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/argon2-linux-x64-gnu": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", @@ -2493,70 +1092,6 @@ "node": ">= 10" } }, - "node_modules/@node-rs/argon2-wasm32-wasi": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", - "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@node-rs/argon2-win32-arm64-msvc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", - "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-win32-ia32-msvc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", - "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-win32-x64-msvc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", - "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/bcrypt": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", @@ -2586,134 +1121,6 @@ "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" } }, - "node_modules/@node-rs/bcrypt-android-arm-eabi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.9.0.tgz", - "integrity": "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-android-arm64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.9.0.tgz", - "integrity": "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-darwin-arm64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.9.0.tgz", - "integrity": "sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-darwin-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.9.0.tgz", - "integrity": "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-freebsd-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.9.0.tgz", - "integrity": "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.9.0.tgz", - "integrity": "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.9.0.tgz", - "integrity": "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.9.0.tgz", - "integrity": "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/bcrypt-linux-x64-gnu": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.9.0.tgz", @@ -2746,103 +1153,6 @@ "node": ">= 10" } }, - "node_modules/@node-rs/bcrypt-wasm32-wasi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.9.0.tgz", - "integrity": "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^0.45.0", - "@emnapi/runtime": "^0.45.0", - "@tybys/wasm-util": "^0.8.1", - "memfs-browser": "^3.4.13000" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/core": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", - "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", - "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz", - "integrity": "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.9.0.tgz", - "integrity": "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-x64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.9.0.tgz", - "integrity": "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4487,125 +2797,6 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", @@ -4640,70 +2831,6 @@ "node": ">= 10" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tailwindcss/postcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", @@ -4751,16 +2878,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/better-sqlite3": { "version": "7.6.12", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", @@ -5332,175 +3449,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", @@ -5527,61 +3475,6 @@ "linux" ] }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6309,6 +4202,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -6455,15 +4349,6 @@ "node": ">=6" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -6480,20 +4365,6 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6947,15 +4818,6 @@ "node": ">=0.4.0" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8545,28 +6407,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", - "license": "Unlicense", - "optional": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8835,6 +6675,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9122,30 +6963,6 @@ "tslib": "^2.8.0" } }, - "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9606,6 +7423,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -9887,132 +7705,6 @@ "lightningcss-win32-x64-msvc": "1.30.1" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", @@ -10055,48 +7747,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10112,24 +7762,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10288,29 +7926,6 @@ "node": ">= 0.6" } }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "license": "Unlicense", - "optional": true, - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/memfs-browser": { - "version": "3.5.10302", - "resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz", - "integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==", - "license": "Unlicense", - "optional": true, - "dependencies": { - "memfs": "3.5.3" - } - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -10481,6 +8096,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10493,6 +8109,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -13617,26 +11234,6 @@ "@node-rs/bcrypt": "1.9.0" } }, - "node_modules/oslo/node_modules/@emnapi/core": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", - "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/oslo/node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/oslo/node_modules/@node-rs/argon2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz", @@ -13662,134 +11259,6 @@ "@node-rs/argon2-win32-x64-msvc": "1.7.0" } }, - "node_modules/oslo/node_modules/@node-rs/argon2-android-arm-eabi": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", - "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-android-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", - "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-darwin-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz", - "integrity": "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-darwin-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", - "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-freebsd-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", - "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", - "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", - "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", - "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz", @@ -13822,83 +11291,6 @@ "node": ">= 10" } }, - "node_modules/oslo/node_modules/@node-rs/argon2-wasm32-wasi": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", - "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^0.45.0", - "@emnapi/runtime": "^0.45.0", - "@tybys/wasm-util": "^0.8.1", - "memfs-browser": "^3.4.13000" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-arm64-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", - "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-ia32-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", - "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-x64-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", - "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@tybys/wasm-util": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", - "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -14470,18 +11862,6 @@ "node": ">= 0.6" } }, - "node_modules/rate-limit-redis": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.1.tgz", - "integrity": "sha512-JsUsVmRVI6G/XrlYtfGV1NMCbGS/CVYayHkxD5Ism5FaL8qpFHCXbFkUeIi5WJ/onJOKWCgtB/xtCLa6qSXb4g==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "peerDependencies": { - "express-rate-limit": ">= 6" - } - }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -14828,27 +12208,6 @@ "node": ">=0.8.8" } }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15240,49 +12599,6 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15628,12 +12944,6 @@ "node": "*" } }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -16021,6 +13331,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16667,6 +13978,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16972,6 +14284,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/tsconfig.json b/tsconfig.json index 24a1cf09..7af98e53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,7 @@ "name": "next" } ], - "target": "ES2017" + "target": "ES2022" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] From d38656e026fbaa9b751c3beeaabcb92a02f91032 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 30 Jul 2025 21:31:16 -0700 Subject: [PATCH 004/219] add clients to int api --- docker-compose.yml | 3 +- messages/en-US.json | 5 ++ server/middlewares/integration/index.ts | 1 + .../integration/verifyApiKeyClientAccess.ts | 86 +++++++++++++++++++ server/routers/client/getClient.ts | 18 ++-- server/routers/client/pickClientDefaults.ts | 2 +- server/routers/integration.ts | 47 +++++++++- .../[orgId]/settings/clients/create/page.tsx | 2 +- src/components/PermissionsSelectBox.tsx | 8 ++ 9 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 server/middlewares/integration/verifyApiKeyClientAccess.ts diff --git a/docker-compose.yml b/docker-compose.yml index 49713379..09b150d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - "3000:3000" - "3001:3001" - "3002:3002" + - "3003:3003" environment: - NODE_ENV=development - ENVIRONMENT=dev @@ -26,4 +27,4 @@ services: - ./postcss.config.mjs:/app/postcss.config.mjs - ./eslint.config.js:/app/eslint.config.js - ./config:/app/config - restart: no \ No newline at end of file + restart: no diff --git a/messages/en-US.json b/messages/en-US.json index e69e2b46..9986c5fd 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Delete IDP Org Policy", "actionListIdpOrgs": "List IDP Orgs", "actionUpdateIdpOrg": "Update IDP Org", + "actionCreateClient": "Create Client", + "actionDeleteClient": "Delete Client", + "actionUpdateClient": "Update Client", + "actionListClients": "List Clients", + "actionGetClient": "Get Client", "noneSelected": "None selected", "orgNotFound2": "No organizations found.", "searchProgress": "Search...", diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 19bf128e..4caf017b 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -10,3 +10,4 @@ export * from "./verifyApiKeySetResourceUsers"; export * from "./verifyAccessTokenAccess"; export * from "./verifyApiKeyIsRoot"; export * from "./verifyApiKeyApiKeyAccess"; +export * from "./verifyApiKeyClientAccess"; diff --git a/server/middlewares/integration/verifyApiKeyClientAccess.ts b/server/middlewares/integration/verifyApiKeyClientAccess.ts new file mode 100644 index 00000000..3583ef4d --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyClientAccess.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from "express"; +import { clients, db } from "@server/db"; +import { apiKeyOrg } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyClientAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const clientId = parseInt( + req.params.clientId || req.body.clientId || req.query.clientId + ); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (isNaN(clientId)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID") + ); + } + + const client = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + if (client.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Client with ID ${clientId} not found` + ) + ); + } + + if (!client[0].orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Client with ID ${clientId} does not have an organization ID` + ) + ); + } + + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, client[0].orgId) + ) + ); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site access" + ) + ); + } +} diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 46f31b8c..6700d97c 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -18,28 +18,28 @@ const getClientSchema = z }) .strict(); -async function query(clientId: number) { +async function query(clientId: number, orgId: string) { // Get the client const [client] = await db .select() .from(clients) - .where(eq(clients.clientId, clientId)) + .where(and(eq(clients.clientId, clientId), eq(clients.orgId, orgId))) .limit(1); - + if (!client) { return null; } - + // Get the siteIds associated with this client const sites = await db .select({ siteId: clientSites.siteId }) .from(clientSites) .where(eq(clientSites.clientId, clientId)); - + // Add the siteIds to the client object return { ...client, - siteIds: sites.map(site => site.siteId) + siteIds: sites.map((site) => site.siteId) }; } @@ -75,9 +75,9 @@ export async function getClient( ); } - const { clientId } = parsedParams.data; + const { clientId, orgId } = parsedParams.data; - const client = await query(clientId); + const client = await query(clientId, orgId); if (!client) { return next( @@ -98,4 +98,4 @@ export async function getClient( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index b1459400..6f452142 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -23,7 +23,7 @@ const pickClientDefaultsSchema = z registry.registerPath({ method: "get", - path: "/site/{siteId}/pick-client-defaults", + path: "/org/{orgId}/pick-client-defaults", description: "Return pre-requisite data for creating a client.", tags: [OpenAPITags.Client, OpenAPITags.Site], request: { diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 51604a11..64675676 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -5,7 +5,7 @@ import * as domain from "./domain"; import * as target from "./target"; import * as user from "./user"; import * as role from "./role"; -// import * as client from "./client"; +import * as client from "./client"; import * as accessToken from "./accessToken"; import * as apiKeys from "./apiKeys"; import * as idp from "./idp"; @@ -20,7 +20,8 @@ import { verifyApiKeyUserAccess, verifyApiKeySetResourceUsers, verifyApiKeyAccessTokenAccess, - verifyApiKeyIsRoot + verifyApiKeyIsRoot, + verifyApiKeyClientAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -513,3 +514,45 @@ authenticated.get( verifyApiKeyHasAction(ActionsEnum.listIdpOrgs), idp.listIdpOrgPolicies ); + +authenticated.get( + "/org/:orgId/pick-client-defaults", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createClient), + client.pickClientDefaults +); + +authenticated.get( + "/org/:orgId/clients", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listClients), + client.listClients +); + +authenticated.get( + "/org/:orgId/client/:clientId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.getClient), + client.getClient +); + +authenticated.put( + "/org/:orgId/client", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createClient), + client.createClient +); + +authenticated.delete( + "/client/:clientId", + verifyApiKeyClientAccess, + verifyApiKeyHasAction(ActionsEnum.deleteClient), + client.deleteClient +); + +authenticated.post( + "/client/:clientId", + verifyApiKeyClientAccess, + verifyApiKeyHasAction(ActionsEnum.updateClient), + client.updateClient +); diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 00b6b34c..2497d3f8 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -705,4 +705,4 @@ export default function Page() { )} ); -} \ No newline at end of file +} diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 6f11d98e..848d116c 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -82,6 +82,14 @@ function getActionsCategories(root: boolean) { [t('actionDeleteResourceRule')]: "deleteResourceRule", [t('actionListResourceRules')]: "listResourceRules", [t('actionUpdateResourceRule')]: "updateResourceRule" + }, + + "Client": { + [t('actionCreateClient')]: "createClient", + [t('actionDeleteClient')]: "deleteClient", + [t('actionUpdateClient')]: "updateClient", + [t('actionListClients')]: "listClients", + [t('actionGetClient')]: "getClient" } }; From 481714f095d2e8a4a1e34207bb3569194bcc3538 Mon Sep 17 00:00:00 2001 From: T Aviss Date: Wed, 30 Jul 2025 22:16:46 -0700 Subject: [PATCH 005/219] Fix for issues with binding ports other than 80/443 server/routers/badger/verifySession.ts : verifyResourceSession() updated code behind "cleanHost" var to a regex which strips the trailing :port for any port (rather than a string match for 80/443) src/app/auth/resource/[resourceId]/page.tsx : ResourceAuthPage() added a secondary match for serverResourceHost and redirectHost that accounts for ports server/routers/badger/exchangeSession.ts : Updated exchangeSession() to use the same "cleanHost" type var (with port-stripping) as in verifyResourceSession(), replaced references to "host" with "cleanHost" --- server/routers/badger/exchangeSession.ts | 10 ++++++++-- server/routers/badger/verifySession.ts | 9 ++++----- src/app/auth/resource/[resourceId]/page.tsx | 5 +++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index 8139694a..b4289281 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -52,20 +52,26 @@ export async function exchangeSession( try { const { requestToken, host, requestIp } = parsedBody.data; + let cleanHost = host; + // if the host ends with :port + if (cleanHost.match(/:[0-9]{1,5}$/)) { + let matched = ''+cleanHost.match(/:[0-9]{1,5}$/); + cleanHost = cleanHost.slice(0, -1*matched.length); + } const clientIp = requestIp?.split(":")[0]; const [resource] = await db .select() .from(resources) - .where(eq(resources.fullDomain, host)) + .where(eq(resources.fullDomain, cleanHost)) .limit(1); if (!resource) { return next( createHttpError( HttpCode.NOT_FOUND, - `Resource with host ${host} not found` + `Resource with host ${cleanHost} not found` ) ); } diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 7ee431d6..48d7c064 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -121,11 +121,10 @@ export async function verifyResourceSession( logger.debug("Client IP:", { clientIp }); let cleanHost = host; - // if the host ends with :443 or :80 remove it - if (cleanHost.endsWith(":443")) { - cleanHost = cleanHost.slice(0, -4); - } else if (cleanHost.endsWith(":80")) { - cleanHost = cleanHost.slice(0, -3); + // if the host ends with :port, strip it + if (cleanHost.match(/:[0-9]{1,5}$/)) { + let matched = ''+cleanHost.match(/:[0-9]{1,5}$/); + cleanHost = cleanHost.slice(0, -1*matched.length); } const resourceCacheKey = `resource:${cleanHost}`; diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index ad5afa0f..9032ae18 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -59,9 +59,14 @@ export default async function ResourceAuthPage(props: { try { const serverResourceHost = new URL(authInfo.url).host; const redirectHost = new URL(searchParams.redirect).host; + const redirectPort = new URL(searchParams.redirect).port; + const serverResourceHostWithPort = `${serverResourceHost}:${redirectPort}`; + if (serverResourceHost === redirectHost) { redirectUrl = searchParams.redirect; + } else if ( serverResourceHostWithPort === redirectHost ) { + redirectUrl = searchParams.redirect; } } catch (e) {} } From b351520e921af1b3374dcdfe0ee9f609ec14d0b8 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 30 Jul 2025 23:18:51 -0700 Subject: [PATCH 006/219] add clients enabled middleware --- server/routers/integration.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 64675676..39939e1c 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -21,7 +21,8 @@ import { verifyApiKeySetResourceUsers, verifyApiKeyAccessTokenAccess, verifyApiKeyIsRoot, - verifyApiKeyClientAccess + verifyApiKeyClientAccess, + verifyClientsEnabled } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -517,6 +518,7 @@ authenticated.get( authenticated.get( "/org/:orgId/pick-client-defaults", + verifyClientsEnabled, verifyApiKeyOrgAccess, verifyApiKeyHasAction(ActionsEnum.createClient), client.pickClientDefaults @@ -524,6 +526,7 @@ authenticated.get( authenticated.get( "/org/:orgId/clients", + verifyClientsEnabled, verifyApiKeyOrgAccess, verifyApiKeyHasAction(ActionsEnum.listClients), client.listClients @@ -531,6 +534,7 @@ authenticated.get( authenticated.get( "/org/:orgId/client/:clientId", + verifyClientsEnabled, verifyApiKeyOrgAccess, verifyApiKeyHasAction(ActionsEnum.getClient), client.getClient @@ -538,6 +542,7 @@ authenticated.get( authenticated.put( "/org/:orgId/client", + verifyClientsEnabled, verifyApiKeyOrgAccess, verifyApiKeyHasAction(ActionsEnum.createClient), client.createClient @@ -545,6 +550,7 @@ authenticated.put( authenticated.delete( "/client/:clientId", + verifyClientsEnabled, verifyApiKeyClientAccess, verifyApiKeyHasAction(ActionsEnum.deleteClient), client.deleteClient @@ -552,6 +558,7 @@ authenticated.delete( authenticated.post( "/client/:clientId", + verifyClientsEnabled, verifyApiKeyClientAccess, verifyApiKeyHasAction(ActionsEnum.updateClient), client.updateClient From 69802e78f8fd0e225410fd59d350a719da25f704 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 11:04:15 -0700 Subject: [PATCH 007/219] Org is not optional --- server/routers/client/getClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 6700d97c..8f01e87d 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -14,7 +14,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const getClientSchema = z .object({ clientId: z.string().transform(stoi).pipe(z.number().int().positive()), - orgId: z.string().optional() + orgId: z.string() }) .strict(); From f0138fad4f93582a0dadf3c57afb1b95d308893e Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 14:25:22 -0700 Subject: [PATCH 008/219] Improve gerbil logging --- server/routers/client/updateClient.ts | 4 +- server/routers/gerbil/peers.ts | 67 +++++++++++++------ server/routers/gerbil/updateHolePunch.ts | 4 +- server/routers/newt/handleGetConfigMessage.ts | 4 +- 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 0dd75186..0fbb2a47 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -341,8 +341,8 @@ export async function updateClient( }); } catch (error) { if (axios.isAxiosError(error)) { - throw new Error( - `Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}` + logger.error( + `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${error.message}` ); } throw error; diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts index 70c56e04..e62c7c8f 100644 --- a/server/routers/gerbil/peers.ts +++ b/server/routers/gerbil/peers.ts @@ -1,15 +1,24 @@ -import axios from 'axios'; -import logger from '@server/logger'; +import axios from "axios"; +import logger from "@server/logger"; import { db } from "@server/db"; -import { exitNodes } from '@server/db'; -import { eq } from 'drizzle-orm'; +import { exitNodes } from "@server/db"; +import { eq } from "drizzle-orm"; -export async function addPeer(exitNodeId: number, peer: { - publicKey: string; - allowedIps: string[]; -}) { - logger.info(`Adding peer with public key ${peer.publicKey} to exit node ${exitNodeId}`); - const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1); +export async function addPeer( + exitNodeId: number, + peer: { + publicKey: string; + allowedIps: string[]; + } +) { + logger.info( + `Adding peer with public key ${peer.publicKey} to exit node ${exitNodeId}` + ); + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, exitNodeId)) + .limit(1); if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); } @@ -18,25 +27,37 @@ export async function addPeer(exitNodeId: number, peer: { } try { - const response = await axios.post(`${exitNode.reachableAt}/peer`, peer, { - headers: { - 'Content-Type': 'application/json', + const response = await axios.post( + `${exitNode.reachableAt}/peer`, + peer, + { + headers: { + "Content-Type": "application/json" + } } - }); + ); - logger.info('Peer added successfully:', { peer: response.data.status }); + logger.info("Peer added successfully:", { peer: response.data.status }); return response.data; } catch (error) { if (axios.isAxiosError(error)) { - throw new Error(`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`); + logger.error( + `Error adding peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` + ); } throw error; } } export async function deletePeer(exitNodeId: number, publicKey: string) { - logger.info(`Deleting peer with public key ${publicKey} from exit node ${exitNodeId}`); - const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1); + logger.info( + `Deleting peer with public key ${publicKey} from exit node ${exitNodeId}` + ); + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, exitNodeId)) + .limit(1); if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); } @@ -44,12 +65,16 @@ export async function deletePeer(exitNodeId: number, publicKey: string) { throw new Error(`Exit node with ID ${exitNodeId} is not reachable`); } try { - const response = await axios.delete(`${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}`); - logger.info('Peer deleted successfully:', response.data.status); + const response = await axios.delete( + `${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}` + ); + logger.info("Peer deleted successfully:", response.data.status); return response.data; } catch (error) { if (axios.isAxiosError(error)) { - throw new Error(`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`); + logger.error( + `Error deleting peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` + ); } throw error; } diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 6d64249c..74c4611b 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -203,8 +203,8 @@ export async function updateHolePunch( }); } catch (error) { if (axios.isAxiosError(error)) { - throw new Error( - `Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}` + logger.error( + `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${error.message}` ); } throw error; diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 2d6ed98b..72e82e23 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -128,8 +128,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { }); } catch (error) { if (axios.isAxiosError(error)) { - throw new Error( - `Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}` + logger.error( + `Error updating proxy mapping (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` ); } throw error; From 36d0b83ed38e98103061665b2bf0413b0227dc0b Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 15:00:17 -0700 Subject: [PATCH 009/219] Fix errors again --- server/routers/client/updateClient.ts | 7 +++++-- server/routers/gerbil/peers.ts | 10 ++++++++-- server/routers/gerbil/updateHolePunch.ts | 7 +++++-- server/routers/newt/handleGetConfigMessage.ts | 5 ++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 0fbb2a47..60a48732 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -342,10 +342,13 @@ export async function updateClient( } catch (error) { if (axios.isAxiosError(error)) { logger.error( - `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${error.message}` + `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}` + ); + } else { + logger.error( + `Error updating destinations for exit node at ${destination.reachableAt}: ${error}` ); } - throw error; } } } diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts index e62c7c8f..40203c41 100644 --- a/server/routers/gerbil/peers.ts +++ b/server/routers/gerbil/peers.ts @@ -44,8 +44,11 @@ export async function addPeer( logger.error( `Error adding peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` ); + } else { + logger.error( + `Error adding peer for exit node at ${exitNode.reachableAt}: ${error}` + ); } - throw error; } } @@ -75,7 +78,10 @@ export async function deletePeer(exitNodeId: number, publicKey: string) { logger.error( `Error deleting peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` ); + } else { + logger.error( + `Error deleting peer for exit node at ${exitNode.reachableAt}: ${error}` + ); } - throw error; } } diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 74c4611b..bb7b53df 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -204,10 +204,13 @@ export async function updateHolePunch( } catch (error) { if (axios.isAxiosError(error)) { logger.error( - `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${error.message}` + `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}` + ); + } else { + logger.error( + `Error updating destinations for exit node at ${destination.reachableAt}: ${error}` ); } - throw error; } } diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 72e82e23..1059847c 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -131,8 +131,11 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { logger.error( `Error updating proxy mapping (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` ); + } else { + logger.error( + `Error updating proxy mapping for exit node at ${exitNode.reachableAt}: ${error}` + ); } - throw error; } } } From 9b1cd5f79c4ed64d0fdec42ff5f425ba3cb5cf21 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 15:01:29 -0700 Subject: [PATCH 010/219] Ignore the config dir --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 042dcf2f..a883e89c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,3 +28,4 @@ LICENSE CONTRIBUTING.md dist .git +config/ \ No newline at end of file From 0151f8a6a91cf9aa4c45752eb7dd530e2ba0d4cd Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 15:57:30 -0700 Subject: [PATCH 011/219] Fix bad sourcePort --- server/routers/gerbil/updateHolePunch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index bb7b53df..836061d6 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -188,7 +188,7 @@ export async function updateHolePunch( `${destination.reachableAt}/update-destinations`, { sourceIp: client.endpoint?.split(":")[0] || "", - sourcePort: client.endpoint?.split(":")[1] || 0, + sourcePort: parseInt(client.endpoint?.split(":")[1] || "0"), destinations: destination.destinations }, { @@ -220,7 +220,7 @@ export async function updateHolePunch( )?.destinations || []; } else if (newtId) { - logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`); + logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`); const { session, newt: newtSession } = await validateNewtSessionToken(token); From ea6f803e78362c1a99fb453fe77f9345954d2ac7 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 17:51:30 -0700 Subject: [PATCH 012/219] Add createdAt to org --- server/db/pg/schema.ts | 3 ++- server/db/sqlite/schema.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index b9228286..be4e58e2 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -23,7 +23,8 @@ export const domains = pgTable("domains", { export const orgs = pgTable("orgs", { orgId: varchar("orgId").primaryKey(), name: varchar("name").notNull(), - subnet: varchar("subnet") + subnet: varchar("subnet"), + createdAt: text("createdAt") }); export const orgDomains = pgTable("orgDomains", { diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 974faa67..5773a5f3 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -16,7 +16,8 @@ export const domains = sqliteTable("domains", { export const orgs = sqliteTable("orgs", { orgId: text("orgId").primaryKey(), name: text("name").notNull(), - subnet: text("subnet") + subnet: text("subnet"), + createdAt: text("createdAt") }); export const userDomains = sqliteTable("userDomains", { From 6d359b6bb95042ac8d027fabc259edadb9b417ee Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 17:53:11 -0700 Subject: [PATCH 013/219] Add createdAt to org insert --- server/routers/org/createOrg.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 9ac65115..8adc2ecb 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -136,7 +136,8 @@ export async function createOrg( .values({ orgId, name, - subnet + subnet, + createdAt: new Date().toISOString() }) .returning(); From f75169fc26b5d577cf4a92259ff4f85d0bdc4247 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 1 Aug 2025 11:08:30 -0700 Subject: [PATCH 014/219] Add missing langs --- src/i18n/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 9871a199..08e43668 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -1,4 +1,4 @@ export type Locale = (typeof locales)[number]; -export const locales = ['en-US', 'es-ES', 'fr-FR', 'de-DE', 'nl-NL', 'it-IT', 'pl-PL', 'pt-PT', 'tr-TR', 'zh-CN', 'ko-KR'] as const; +export const locales = ['en-US', 'es-ES', 'fr-FR', 'de-DE', 'nl-NL', 'it-IT', 'pl-PL', 'pt-PT', 'tr-TR', 'zh-CN', 'ko-KR', 'bg-BG', 'cs-CZ', 'ru-RU'] as const; export const defaultLocale: Locale = 'en-US'; \ No newline at end of file From e85b772ca5f14505ea80268e42fa9021e6c961e1 Mon Sep 17 00:00:00 2001 From: Marvin <127591405+Lokowitz@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:33:25 +0000 Subject: [PATCH 015/219] update versions --- .github/dependabot.yml | 22 ++++++++++++++++++++++ docker-compose.example.yml | 2 +- install/config/crowdsec/traefik_config.yml | 2 +- install/config/docker-compose.yml | 2 +- install/go.mod | 6 +++--- install/go.sum | 8 ++++---- 6 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d17590ef..196676e9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -38,3 +38,25 @@ updates: directory: "/" schedule: interval: "weekly" + + - package-ecosystem: "gomod" + directory: "/install" + schedule: + interval: "daily" + groups: + dev-patch-updates: + dependency-type: "development" + update-types: + - "patch" + dev-minor-updates: + dependency-type: "development" + update-types: + - "minor" + prod-patch-updates: + dependency-type: "production" + update-types: + - "patch" + prod-minor-updates: + dependency-type: "production" + update-types: + - "minor" \ No newline at end of file diff --git a/docker-compose.example.yml b/docker-compose.example.yml index c7c068f0..703c47c6 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -36,7 +36,7 @@ services: - 80:80 # Port for traefik because of the network_mode traefik: - image: traefik:v3.4.0 + image: traefik:v3.5 container_name: traefik restart: unless-stopped network_mode: service:gerbil # Ports appear on the gerbil service diff --git a/install/config/crowdsec/traefik_config.yml b/install/config/crowdsec/traefik_config.yml index 7ccfd7cf..198693ef 100644 --- a/install/config/crowdsec/traefik_config.yml +++ b/install/config/crowdsec/traefik_config.yml @@ -16,7 +16,7 @@ experimental: version: "{{.BadgerVersion}}" crowdsec: # CrowdSec plugin configuration added moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - version: "v1.4.2" + version: "v1.4.4" log: level: "INFO" diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 4ce31e41..a9053c62 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -36,7 +36,7 @@ services: - 80:80 # Port for traefik because of the network_mode {{end}} traefik: - image: docker.io/traefik:v3.4.1 + image: docker.io/traefik:v3.5 container_name: traefik restart: unless-stopped {{if .InstallGerbil}} diff --git a/install/go.mod b/install/go.mod index 1d12aa12..37b815e8 100644 --- a/install/go.mod +++ b/install/go.mod @@ -1,10 +1,10 @@ module installer -go 1.23.0 +go 1.24 require ( - golang.org/x/term v0.28.0 + golang.org/x/term v0.33.0 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.29.0 // indirect +require golang.org/x/sys v0.34.0 // indirect diff --git a/install/go.sum b/install/go.sum index 169165e4..71a81b94 100644 --- a/install/go.sum +++ b/install/go.sum @@ -1,7 +1,7 @@ -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 529d1c9f667b05cf0a58ca64632fbb4f30840a17 Mon Sep 17 00:00:00 2001 From: Marvin <127591405+Lokowitz@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:37:08 +0000 Subject: [PATCH 016/219] modified: .github/workflows/cicd.yml --- .github/workflows/cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index c21b8985..5be89da3 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -30,7 +30,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.23.0 + go-version: 1.24 - name: Update version in package.json run: | From 7402590f49e9ac8ac8df8103898fbbead471909a Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 1 Aug 2025 15:55:47 -0700 Subject: [PATCH 017/219] remove api-key-org association for root keys --- .../integration/verifyApiKeyApiKeyAccess.ts | 5 +++++ .../integration/verifyApiKeyClientAccess.ts | 5 +++++ .../integration/verifyApiKeyOrgAccess.ts | 5 +++++ .../integration/verifyApiKeyResourceAccess.ts | 5 +++++ .../integration/verifyApiKeyRoleAccess.ts | 5 +++++ .../integration/verifyApiKeySetResourceUsers.ts | 5 +++++ .../integration/verifyApiKeySiteAccess.ts | 10 ++++++---- .../integration/verifyApiKeyTargetAccess.ts | 5 +++++ .../integration/verifyApiKeyUserAccess.ts | 5 +++++ server/routers/apiKeys/createRootApiKey.ts | 9 --------- server/routers/org/createOrg.ts | 14 +------------- 11 files changed, 47 insertions(+), 26 deletions(-) diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts index 1441589d..ad5b7fc4 100644 --- a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts +++ b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts @@ -35,6 +35,11 @@ export async function verifyApiKeyApiKeyAccess( ); } + if (callerApiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + const [callerApiKeyOrg] = await db .select() .from(apiKeyOrg) diff --git a/server/middlewares/integration/verifyApiKeyClientAccess.ts b/server/middlewares/integration/verifyApiKeyClientAccess.ts index 3583ef4d..e5ed624d 100644 --- a/server/middlewares/integration/verifyApiKeyClientAccess.ts +++ b/server/middlewares/integration/verifyApiKeyClientAccess.ts @@ -28,6 +28,11 @@ export async function verifyApiKeyClientAccess( ); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + const client = await db .select() .from(clients) diff --git a/server/middlewares/integration/verifyApiKeyOrgAccess.ts b/server/middlewares/integration/verifyApiKeyOrgAccess.ts index 84ba7fe9..c705dc0f 100644 --- a/server/middlewares/integration/verifyApiKeyOrgAccess.ts +++ b/server/middlewares/integration/verifyApiKeyOrgAccess.ts @@ -27,6 +27,11 @@ export async function verifyApiKeyOrgAccess( ); } + if (req.apiKey?.isRoot) { + // Root keys can access any key in any org + return next(); + } + if (!req.apiKeyOrg) { const apiKeyOrgRes = await db .select() diff --git a/server/middlewares/integration/verifyApiKeyResourceAccess.ts b/server/middlewares/integration/verifyApiKeyResourceAccess.ts index 2473c814..184ee73c 100644 --- a/server/middlewares/integration/verifyApiKeyResourceAccess.ts +++ b/server/middlewares/integration/verifyApiKeyResourceAccess.ts @@ -37,6 +37,11 @@ export async function verifyApiKeyResourceAccess( ); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + if (!resource.orgId) { return next( createHttpError( diff --git a/server/middlewares/integration/verifyApiKeyRoleAccess.ts b/server/middlewares/integration/verifyApiKeyRoleAccess.ts index 0df10913..ffe223a6 100644 --- a/server/middlewares/integration/verifyApiKeyRoleAccess.ts +++ b/server/middlewares/integration/verifyApiKeyRoleAccess.ts @@ -45,6 +45,11 @@ export async function verifyApiKeyRoleAccess( ); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + const orgIds = new Set(rolesData.map((role) => role.orgId)); for (const role of rolesData) { diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts index cbb2b598..9c96e6ec 100644 --- a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts +++ b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts @@ -32,6 +32,11 @@ export async function verifyApiKeySetResourceUsers( return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs")); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + if (userIds.length === 0) { return next(); } diff --git a/server/middlewares/integration/verifyApiKeySiteAccess.ts b/server/middlewares/integration/verifyApiKeySiteAccess.ts index 35ec3b6a..0a310d15 100644 --- a/server/middlewares/integration/verifyApiKeySiteAccess.ts +++ b/server/middlewares/integration/verifyApiKeySiteAccess.ts @@ -1,9 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { - sites, - apiKeyOrg -} from "@server/db"; +import { sites, apiKeyOrg } from "@server/db"; import { and, eq, or } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -31,6 +28,11 @@ export async function verifyApiKeySiteAccess( ); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + const site = await db .select() .from(sites) diff --git a/server/middlewares/integration/verifyApiKeyTargetAccess.ts b/server/middlewares/integration/verifyApiKeyTargetAccess.ts index f810e4a2..71146c15 100644 --- a/server/middlewares/integration/verifyApiKeyTargetAccess.ts +++ b/server/middlewares/integration/verifyApiKeyTargetAccess.ts @@ -66,6 +66,11 @@ export async function verifyApiKeyTargetAccess( ); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + if (!resource.orgId) { return next( createHttpError( diff --git a/server/middlewares/integration/verifyApiKeyUserAccess.ts b/server/middlewares/integration/verifyApiKeyUserAccess.ts index 070ae5ac..a69489bf 100644 --- a/server/middlewares/integration/verifyApiKeyUserAccess.ts +++ b/server/middlewares/integration/verifyApiKeyUserAccess.ts @@ -27,6 +27,11 @@ export async function verifyApiKeyUserAccess( ); } + if (apiKey.isRoot) { + // Root keys can access any key in any org + return next(); + } + if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) { return next( createHttpError( diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts index 095d952b..0754574a 100644 --- a/server/routers/apiKeys/createRootApiKey.ts +++ b/server/routers/apiKeys/createRootApiKey.ts @@ -63,15 +63,6 @@ export async function createRootApiKey( lastChars, isRoot: true }); - - const allOrgs = await trx.select().from(orgs); - - for (const org of allOrgs) { - await trx.insert(apiKeyOrg).values({ - apiKeyId, - orgId: org.orgId - }); - } }); try { diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 8adc2ecb..d26774dd 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -215,7 +215,7 @@ export async function createOrg( orgId: newOrg[0].orgId, roleId: roleId, isOwner: true - }); + }); } const memberRole = await trx @@ -234,18 +234,6 @@ export async function createOrg( orgId })) ); - - const rootApiKeys = await trx - .select() - .from(apiKeys) - .where(eq(apiKeys.isRoot, true)); - - for (const apiKey of rootApiKeys) { - await trx.insert(apiKeyOrg).values({ - apiKeyId: apiKey.apiKeyId, - orgId: newOrg[0].orgId - }); - } }); if (!org) { From 84268e484db6b92e4e13a968f6ef8b5671da034c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 1 Aug 2025 22:34:02 -0700 Subject: [PATCH 018/219] update docs links --- CONTRIBUTING.md | 4 ++-- README.md | 4 ++-- config/config.example.yml | 2 +- install/config/config.yml | 2 +- install/main.go | 2 +- server/emails/templates/WelcomeQuickStart.tsx | 2 +- server/lib/config.ts | 6 ------ server/lib/readConfigFile.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta9.ts | 2 +- server/setup/scriptsSqlite/1.2.0.ts | 2 +- src/app/[orgId]/settings/resources/create/page.tsx | 2 +- src/app/[orgId]/settings/sites/SitesSplashCard.tsx | 2 +- src/app/[orgId]/settings/sites/[niceId]/general/page.tsx | 6 +++--- src/app/admin/idp/[idpId]/policies/page.tsx | 2 +- src/components/LayoutSidebar.tsx | 2 +- src/components/SupporterStatus.tsx | 2 +- 16 files changed, 19 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 179cd86d..9bd2bc67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Contributions are welcome! Please see the contribution and local development guide on the docs page before getting started: -https://docs.fossorial.io/development +https://docs.digpangolin.com/development/contributing ### Licensing Considerations @@ -17,4 +17,4 @@ By creating this pull request, I grant the project maintainers an unlimited, perpetual license to use, modify, and redistribute these contributions under any terms they choose, including both the AGPLv3 and the Fossorial Commercial license terms. I represent that I have the right to grant this license for all contributed content. -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index 8c94815d..c09fdb24 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ _Pangolin tunnels your services to the internet so you can access anything from Website | - + Install Guide | @@ -104,7 +104,7 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and access ### Fully Self Hosted -Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.fossorial.io/Getting%20Started/quick-install) to get started. +Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started. > Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal! diff --git a/config/config.example.yml b/config/config.example.yml index 5a78ae5e..c5f70641 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,5 +1,5 @@ # To see all available options, please visit the docs: -# https://docs.fossorial.io/Pangolin/Configuration/config +# https://docs.digpangolin.com/self-host/advanced/config-file app: dashboard_url: "http://localhost:3002" diff --git a/install/config/config.yml b/install/config/config.yml index 00d7c897..2928b425 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,5 +1,5 @@ # To see all available options, please visit the docs: -# https://docs.fossorial.io/Pangolin/Configuration/config +# https://docs.digpangolin.com/self-host/dns-and-networking app: dashboard_url: "https://{{.DashboardDomain}}" diff --git a/install/main.go b/install/main.go index 9bb0c7e1..a6d9d686 100644 --- a/install/main.go +++ b/install/main.go @@ -70,7 +70,7 @@ func main() { fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.") fmt.Println("- Point your domain to the VPS IP with A records.") fmt.Println("") - fmt.Println("http://docs.fossorial.io/Getting%20Started/dns-networking") + fmt.Println("https://docs.digpangolin.com/self-host/dns-and-networking") fmt.Println("") fmt.Println("Lets get started!") fmt.Println("") diff --git a/server/emails/templates/WelcomeQuickStart.tsx b/server/emails/templates/WelcomeQuickStart.tsx index caebff06..cd18f8b5 100644 --- a/server/emails/templates/WelcomeQuickStart.tsx +++ b/server/emails/templates/WelcomeQuickStart.tsx @@ -88,7 +88,7 @@ export const WelcomeQuickStart = ({ To learn how to use Newt, including more installation methods, visit the{" "} docs diff --git a/server/lib/config.ts b/server/lib/config.ts index 0a964469..023ae054 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -30,12 +30,6 @@ export class Config { throw new Error(`Invalid configuration file: ${errors}`); } - if (process.env.APP_BASE_DOMAIN) { - console.log( - "WARNING: You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/" - ); - } - if ( // @ts-ignore parsedConfig.users || diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 42fcefd3..1bc119fa 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -287,7 +287,7 @@ export function readConfigFile() { if (!environment) { throw new Error( - "No configuration file found. Please create one. https://docs.fossorial.io/" + "No configuration file found. Please create one. https://docs.digpangolin.com/self-host/advanced/config-file" ); } diff --git a/server/setup/scriptsSqlite/1.0.0-beta9.ts b/server/setup/scriptsSqlite/1.0.0-beta9.ts index c731996b..bbd61484 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta9.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta9.ts @@ -78,7 +78,7 @@ export default async function migration() { fs.writeFileSync(filePath, updatedYaml, "utf8"); } catch (e) { console.log( - `Failed to add resource_session_request_param to config. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config` + `Failed to add resource_session_request_param to config. Please add it manually. https://docs.digpangolin.com/self-host/advanced/config-file` ); trx.rollback(); return; diff --git a/server/setup/scriptsSqlite/1.2.0.ts b/server/setup/scriptsSqlite/1.2.0.ts index 940d38e6..38bb90b8 100644 --- a/server/setup/scriptsSqlite/1.2.0.ts +++ b/server/setup/scriptsSqlite/1.2.0.ts @@ -63,7 +63,7 @@ export default async function migration() { console.log(`Added new config option: resource_access_token_headers`); } catch (e) { console.log( - `Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config` + `Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.digpangolin.com/self-host/advanced/config-file` ); console.error(e); } diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index fc90d26c..a8d926fe 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -734,7 +734,7 @@ export default function Page() { diff --git a/src/app/[orgId]/settings/sites/SitesSplashCard.tsx b/src/app/[orgId]/settings/sites/SitesSplashCard.tsx index 8bab93e6..35d7bd83 100644 --- a/src/app/[orgId]/settings/sites/SitesSplashCard.tsx +++ b/src/app/[orgId]/settings/sites/SitesSplashCard.tsx @@ -64,7 +64,7 @@ export const SitesSplashCard = () => {

diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 1581d961..f92a5090 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -65,7 +65,7 @@ export default function GeneralPage() { defaultValues: { name: site?.name, dockerSocketEnabled: site?.dockerSocketEnabled ?? false, - remoteSubnets: site?.remoteSubnets + remoteSubnets: site?.remoteSubnets ? site.remoteSubnets.split(',').map((subnet, index) => ({ id: subnet.trim(), text: subnet.trim() @@ -144,7 +144,7 @@ export default function GeneralPage() { )} /> - +
{" "} {t('supportKeyPurchase2')}{" "} Date: Sun, 3 Aug 2025 12:21:41 +0530 Subject: [PATCH 019/219] Default language detection via browser language header --- src/services/locale.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/services/locale.ts b/src/services/locale.ts index 29051152..b4251e72 100644 --- a/src/services/locale.ts +++ b/src/services/locale.ts @@ -1,16 +1,36 @@ 'use server'; -import {cookies} from 'next/headers'; -import {Locale, defaultLocale} from '@/i18n/config'; +import {cookies, headers} from 'next/headers'; +import {Locale, defaultLocale, locales} from '@/i18n/config'; // In this example the locale is read from a cookie. You could alternatively // also read it from a database, backend service, or any other source. const COOKIE_NAME = 'NEXT_LOCALE'; -export async function getUserLocale() { - return (await cookies()).get(COOKIE_NAME)?.value || defaultLocale; +export async function getUserLocale(): Promise { + const cookieLocale = (await cookies()).get(COOKIE_NAME)?.value; + + if (cookieLocale && locales.includes(cookieLocale as Locale)) { + return cookieLocale as Locale; + } + + const headerList = await headers(); + const acceptLang = headerList.get('accept-language'); + + if (acceptLang) { + const browserLang = acceptLang.split(',')[0]; + const matched = locales.find((locale) => + browserLang.toLowerCase().startsWith(locale.split('-')[0].toLowerCase()) + ); + if (matched) { + return matched; + } + } + + return defaultLocale; } + export async function setUserLocale(locale: Locale) { (await cookies()).set(COOKIE_NAME, locale); } From 616dae2d8b0a06f299154f1f82d604378b69653d Mon Sep 17 00:00:00 2001 From: Pallavi Date: Sun, 3 Aug 2025 12:26:21 +0530 Subject: [PATCH 020/219] code format --- src/services/locale.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/locale.ts b/src/services/locale.ts index b4251e72..46e5fd10 100644 --- a/src/services/locale.ts +++ b/src/services/locale.ts @@ -1,7 +1,7 @@ 'use server'; -import {cookies, headers} from 'next/headers'; -import {Locale, defaultLocale, locales} from '@/i18n/config'; +import { cookies, headers } from 'next/headers'; +import { Locale, defaultLocale, locales } from '@/i18n/config'; // In this example the locale is read from a cookie. You could alternatively // also read it from a database, backend service, or any other source. @@ -14,7 +14,7 @@ export async function getUserLocale(): Promise { return cookieLocale as Locale; } - const headerList = await headers(); + const headerList = await headers(); const acceptLang = headerList.get('accept-language'); if (acceptLang) { From bb84d01e1404eaaf758ab4da0cd9f050318931fc Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:47:27 +0800 Subject: [PATCH 021/219] Reset a user's security keys (passkeys) by deleting all their webauthn credentials. pangctl reset-user-security-keys --email user@example.com This command will: 1. Find the user by email address 2. Check if they have any registered security keys 3. Delete all their security keys from the database 4. Provide feedback on the operation --- cli/commands/resetUserSecurityKeys.ts | 67 +++++++++++++++++++++++++++ cli/index.ts | 2 + 2 files changed, 69 insertions(+) create mode 100644 cli/commands/resetUserSecurityKeys.ts diff --git a/cli/commands/resetUserSecurityKeys.ts b/cli/commands/resetUserSecurityKeys.ts new file mode 100644 index 00000000..84af7cec --- /dev/null +++ b/cli/commands/resetUserSecurityKeys.ts @@ -0,0 +1,67 @@ +import { CommandModule } from "yargs"; +import { db, users, securityKeys } from "@server/db"; +import { eq } from "drizzle-orm"; + +type ResetUserSecurityKeysArgs = { + email: string; +}; + +export const resetUserSecurityKeys: CommandModule<{}, ResetUserSecurityKeysArgs> = { + command: "reset-user-security-keys", + describe: "Reset a user's security keys (passkeys) by deleting all their webauthn credentials", + builder: (yargs) => { + return yargs + .option("email", { + type: "string", + demandOption: true, + describe: "User email address" + }); + }, + handler: async (argv: { email: string }) => { + try { + const { email } = argv; + + console.log(`Looking for user with email: ${email}`); + + // Find the user by email + const [user] = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (!user) { + console.error(`User with email '${email}' not found`); + process.exit(1); + } + + console.log(`Found user: ${user.email} (ID: ${user.userId})`); + + // Check if user has any security keys + const userSecurityKeys = await db + .select() + .from(securityKeys) + .where(eq(securityKeys.userId, user.userId)); + + if (userSecurityKeys.length === 0) { + console.log(`User '${email}' has no security keys to reset`); + process.exit(0); + } + + console.log(`Found ${userSecurityKeys.length} security key(s) for user '${email}'`); + + // Delete all security keys for the user + await db + .delete(securityKeys) + .where(eq(securityKeys.userId, user.userId)); + + console.log(`Successfully reset security keys for user '${email}'`); + console.log(`Deleted ${userSecurityKeys.length} security key(s)`); + + process.exit(0); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } + } +}; \ No newline at end of file diff --git a/cli/index.ts b/cli/index.ts index db76dbf9..f9e884cc 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -3,9 +3,11 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { setAdminCredentials } from "@cli/commands/setAdminCredentials"; +import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys"; yargs(hideBin(process.argv)) .scriptName("pangctl") .command(setAdminCredentials) + .command(resetUserSecurityKeys) .demandCommand() .help().argv; From 69baa6785fba33e5466eddcffd88dc4bbb87aa57 Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Sun, 3 Aug 2025 21:17:18 +0800 Subject: [PATCH 022/219] feat: Add setup token security for initial server setup - Add setupTokens database table with proper schema - Implement setup token generation on first server startup - Add token validation endpoint and modify admin creation - Update initial setup page to require setup token - Add migration scripts for both SQLite and PostgreSQL - Add internationalization support for setup token fields - Implement proper error handling and logging - Add CLI command for resetting user security keys This prevents unauthorized access during initial server setup by requiring a token that is generated and displayed in the server console. --- messages/en-US.json | 3 + package-lock.json | 110 ++-------------------- server/db/pg/schema.ts | 9 ++ server/db/sqlite/schema.ts | 9 ++ server/routers/auth/index.ts | 1 + server/routers/auth/setServerAdmin.ts | 59 +++++++++--- server/routers/auth/validateSetupToken.ts | 84 +++++++++++++++++ server/routers/external.ts | 1 + server/setup/ensureSetupToken.ts | 73 ++++++++++++++ server/setup/index.ts | 2 + server/setup/migrationsPg.ts | 4 +- server/setup/migrationsSqlite.ts | 2 + server/setup/scriptsPg/1.9.0.ts | 25 +++++ server/setup/scriptsSqlite/1.9.0.ts | 35 +++++++ src/app/auth/initial-setup/page.tsx | 20 ++++ 15 files changed, 322 insertions(+), 115 deletions(-) create mode 100644 server/routers/auth/validateSetupToken.ts create mode 100644 server/setup/ensureSetupToken.ts create mode 100644 server/setup/scriptsPg/1.9.0.ts create mode 100644 server/setup/scriptsSqlite/1.9.0.ts diff --git a/messages/en-US.json b/messages/en-US.json index 9986c5fd..0389a0dc 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -967,6 +967,9 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", diff --git a/package-lock.json b/package-lock.json index baec0b2b..7eb58d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,6 @@ "http-errors": "2.0.0", "i": "^0.3.7", "input-otp": "1.4.2", - "ioredis": "^5.6.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", @@ -77,7 +76,6 @@ "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", - "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -2010,12 +2008,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "license": "MIT" - }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2058,6 +2050,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -6309,6 +6302,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -6455,15 +6449,6 @@ "node": ">=6" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -6947,15 +6932,6 @@ "node": ">=0.4.0" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8835,6 +8811,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9122,30 +9099,6 @@ "tslib": "^2.8.0" } }, - "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9606,6 +9559,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -10112,24 +10066,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10481,6 +10423,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10493,6 +10436,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -14470,18 +14414,6 @@ "node": ">= 0.6" } }, - "node_modules/rate-limit-redis": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.1.tgz", - "integrity": "sha512-JsUsVmRVI6G/XrlYtfGV1NMCbGS/CVYayHkxD5Ism5FaL8qpFHCXbFkUeIi5WJ/onJOKWCgtB/xtCLa6qSXb4g==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "peerDependencies": { - "express-rate-limit": ">= 6" - } - }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -14828,27 +14760,6 @@ "node": ">=0.8.8" } }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15628,12 +15539,6 @@ "node": "*" } }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -16021,6 +15926,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16667,6 +16573,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16972,6 +16879,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index be4e58e2..afc9d6e7 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -592,6 +592,14 @@ export const webauthnChallenge = pgTable("webauthnChallenge", { expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp }); +export const setupTokens = pgTable("setupTokens", { + tokenId: varchar("tokenId").primaryKey(), + token: varchar("token").notNull(), + used: boolean("used").notNull().default(false), + dateCreated: varchar("dateCreated").notNull(), + dateUsed: varchar("dateUsed") +}); + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -637,3 +645,4 @@ export type OlmSession = InferSelectModel; export type UserClient = InferSelectModel; export type RoleClient = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SetupToken = InferSelectModel; diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 5773a5f3..cc0fb6d0 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -187,6 +187,14 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", { expiresAt: integer("expiresAt").notNull() // Unix timestamp }); +export const setupTokens = sqliteTable("setupTokens", { + tokenId: text("tokenId").primaryKey(), + token: text("token").notNull(), + used: integer("used", { mode: "boolean" }).notNull().default(false), + dateCreated: text("dateCreated").notNull(), + dateUsed: text("dateUsed") +}); + export const newts = sqliteTable("newt", { newtId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), @@ -679,3 +687,4 @@ export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SetupToken = InferSelectModel; diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index cc8fd630..505d12c2 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -10,6 +10,7 @@ export * from "./resetPassword"; export * from "./requestPasswordReset"; export * from "./setServerAdmin"; export * from "./initialSetupComplete"; +export * from "./validateSetupToken"; export * from "./changePassword"; export * from "./checkResourceSession"; export * from "./securityKey"; diff --git a/server/routers/auth/setServerAdmin.ts b/server/routers/auth/setServerAdmin.ts index 7c49753e..ebb95359 100644 --- a/server/routers/auth/setServerAdmin.ts +++ b/server/routers/auth/setServerAdmin.ts @@ -8,14 +8,15 @@ import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; import { passwordSchema } from "@server/auth/passwordSchema"; import { response } from "@server/lib"; -import { db, users } from "@server/db"; -import { eq } from "drizzle-orm"; +import { db, users, setupTokens } from "@server/db"; +import { eq, and } from "drizzle-orm"; import { UserType } from "@server/types/UserTypes"; import moment from "moment"; export const bodySchema = z.object({ email: z.string().toLowerCase().email(), - password: passwordSchema + password: passwordSchema, + setupToken: z.string().min(1, "Setup token is required") }); export type SetServerAdminBody = z.infer; @@ -39,7 +40,27 @@ export async function setServerAdmin( ); } - const { email, password } = parsedBody.data; + const { email, password, setupToken } = parsedBody.data; + + // Validate setup token + const [validToken] = await db + .select() + .from(setupTokens) + .where( + and( + eq(setupTokens.token, setupToken), + eq(setupTokens.used, false) + ) + ); + + if (!validToken) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid or expired setup token" + ) + ); + } const [existing] = await db .select() @@ -58,15 +79,27 @@ export async function setServerAdmin( const passwordHash = await hashPassword(password); const userId = generateId(15); - await db.insert(users).values({ - userId: userId, - email: email, - type: UserType.Internal, - username: email, - passwordHash, - dateCreated: moment().toISOString(), - serverAdmin: true, - emailVerified: true + await db.transaction(async (trx) => { + // Mark the token as used + await trx + .update(setupTokens) + .set({ + used: true, + dateUsed: moment().toISOString() + }) + .where(eq(setupTokens.tokenId, validToken.tokenId)); + + // Create the server admin user + await trx.insert(users).values({ + userId: userId, + email: email, + type: UserType.Internal, + username: email, + passwordHash, + dateCreated: moment().toISOString(), + serverAdmin: true, + emailVerified: true + }); }); return response(res, { diff --git a/server/routers/auth/validateSetupToken.ts b/server/routers/auth/validateSetupToken.ts new file mode 100644 index 00000000..e3c29833 --- /dev/null +++ b/server/routers/auth/validateSetupToken.ts @@ -0,0 +1,84 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, setupTokens } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const validateSetupTokenSchema = z + .object({ + token: z.string().min(1, "Token is required") + }) + .strict(); + +export type ValidateSetupTokenResponse = { + valid: boolean; + message: string; +}; + +export async function validateSetupToken( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = validateSetupTokenSchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { token } = parsedBody.data; + + // Find the token in the database + const [setupToken] = await db + .select() + .from(setupTokens) + .where( + and( + eq(setupTokens.token, token), + eq(setupTokens.used, false) + ) + ); + + if (!setupToken) { + return response(res, { + data: { + valid: false, + message: "Invalid or expired setup token" + }, + success: true, + error: false, + message: "Token validation completed", + status: HttpCode.OK + }); + } + + return response(res, { + data: { + valid: true, + message: "Setup token is valid" + }, + success: true, + error: false, + message: "Token validation completed", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to validate setup token" + ) + ); + } +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 5bae553e..f9ff7377 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -1033,6 +1033,7 @@ authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback); authRouter.put("/set-server-admin", auth.setServerAdmin); authRouter.get("/initial-setup-complete", auth.initialSetupComplete); +authRouter.post("/validate-setup-token", auth.validateSetupToken); // Security Key routes authRouter.post( diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts new file mode 100644 index 00000000..1734b5e6 --- /dev/null +++ b/server/setup/ensureSetupToken.ts @@ -0,0 +1,73 @@ +import { db, setupTokens, users } from "@server/db"; +import { eq } from "drizzle-orm"; +import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; +import moment from "moment"; +import logger from "@server/logger"; + +const random: RandomReader = { + read(bytes: Uint8Array): void { + crypto.getRandomValues(bytes); + } +}; + +function generateToken(): string { + // Generate a 32-character alphanumeric token + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + return generateRandomString(random, alphabet, 32); +} + +function generateId(length: number): string { + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + return generateRandomString(random, alphabet, length); +} + +export async function ensureSetupToken() { + try { + // Check if a server admin already exists + const [existingAdmin] = await db + .select() + .from(users) + .where(eq(users.serverAdmin, true)); + + // If admin exists, no need for setup token + if (existingAdmin) { + logger.warn("Server admin exists. Setup token generation skipped."); + return; + } + + // Check if a setup token already exists + const existingTokens = await db + .select() + .from(setupTokens) + .where(eq(setupTokens.used, false)); + + // If unused token exists, display it instead of creating a new one + if (existingTokens.length > 0) { + console.log("=== SETUP TOKEN EXISTS ==="); + console.log("Token:", existingTokens[0].token); + console.log("Use this token on the initial setup page"); + console.log("================================"); + return; + } + + // Generate a new setup token + const token = generateToken(); + const tokenId = generateId(15); + + await db.insert(setupTokens).values({ + tokenId: tokenId, + token: token, + used: false, + dateCreated: moment().toISOString(), + dateUsed: null + }); + + console.log("=== SETUP TOKEN GENERATED ==="); + console.log("Token:", token); + console.log("Use this token on the initial setup page"); + console.log("================================"); + } catch (error) { + console.error("Failed to ensure setup token:", error); + throw error; + } +} \ No newline at end of file diff --git a/server/setup/index.ts b/server/setup/index.ts index d126869a..2dfb633e 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -1,9 +1,11 @@ import { ensureActions } from "./ensureActions"; import { copyInConfig } from "./copyInConfig"; import { clearStaleData } from "./clearStaleData"; +import { ensureSetupToken } from "./ensureSetupToken"; export async function runSetupFunctions() { await copyInConfig(); // copy in the config to the db as needed await ensureActions(); // make sure all of the actions are in the db and the roles await clearStaleData(); + await ensureSetupToken(); // ensure setup token exists for initial setup } diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 07ece65b..6b3f20b9 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -8,6 +8,7 @@ import path from "path"; import m1 from "./scriptsPg/1.6.0"; import m2 from "./scriptsPg/1.7.0"; import m3 from "./scriptsPg/1.8.0"; +import m4 from "./scriptsPg/1.9.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -16,7 +17,8 @@ import m3 from "./scriptsPg/1.8.0"; const migrations = [ { version: "1.6.0", run: m1 }, { version: "1.7.0", run: m2 }, - { version: "1.8.0", run: m3 } + { version: "1.8.0", run: m3 }, + { version: "1.9.0", run: m4 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 15dd28d2..5b0850c8 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -25,6 +25,7 @@ import m20 from "./scriptsSqlite/1.5.0"; import m21 from "./scriptsSqlite/1.6.0"; import m22 from "./scriptsSqlite/1.7.0"; import m23 from "./scriptsSqlite/1.8.0"; +import m24 from "./scriptsSqlite/1.9.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -49,6 +50,7 @@ const migrations = [ { version: "1.6.0", run: m21 }, { version: "1.7.0", run: m22 }, { version: "1.8.0", run: m23 }, + { version: "1.9.0", run: m24 }, // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts new file mode 100644 index 00000000..22259cae --- /dev/null +++ b/server/setup/scriptsPg/1.9.0.ts @@ -0,0 +1,25 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; + +const version = "1.9.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql` + CREATE TABLE "setupTokens" ( + "tokenId" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "used" boolean DEFAULT false NOT NULL, + "dateCreated" varchar NOT NULL, + "dateUsed" varchar + ); + `); + + console.log(`Added setupTokens table`); + } catch (e) { + console.log("Unable to add setupTokens table:", e); + throw e; + } +} \ No newline at end of file diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts new file mode 100644 index 00000000..a4a20dda --- /dev/null +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -0,0 +1,35 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.9.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + db.pragma("foreign_keys = OFF"); + + db.transaction(() => { + db.exec(` + CREATE TABLE 'setupTokens' ( + 'tokenId' text PRIMARY KEY NOT NULL, + 'token' text NOT NULL, + 'used' integer DEFAULT 0 NOT NULL, + 'dateCreated' text NOT NULL, + 'dateUsed' text + ); + `); + })(); + + db.pragma("foreign_keys = ON"); + + console.log(`Added setupTokens table`); + } catch (e) { + console.log("Unable to add setupTokens table:", e); + throw e; + } +} \ No newline at end of file diff --git a/src/app/auth/initial-setup/page.tsx b/src/app/auth/initial-setup/page.tsx index 17e6c2ec..518c5370 100644 --- a/src/app/auth/initial-setup/page.tsx +++ b/src/app/auth/initial-setup/page.tsx @@ -31,6 +31,7 @@ import { passwordSchema } from "@server/auth/passwordSchema"; const formSchema = z .object({ + setupToken: z.string().min(1, "Setup token is required"), email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, confirmPassword: z.string() @@ -52,6 +53,7 @@ export default function InitialSetupPage() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { + setupToken: "", email: "", password: "", confirmPassword: "" @@ -63,6 +65,7 @@ export default function InitialSetupPage() { setError(null); try { const res = await api.put("/auth/set-server-admin", { + setupToken: values.setupToken, email: values.email, password: values.password }); @@ -102,6 +105,23 @@ export default function InitialSetupPage() { onSubmit={form.handleSubmit(onSubmit)} className="space-y-4" > + ( + + {t("setupToken")} + + + + + + )} + /> Date: Sun, 3 Aug 2025 21:20:25 +0800 Subject: [PATCH 023/219] revert: package-lock.json to original state --- package-lock.json | 110 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7eb58d2c..baec0b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "http-errors": "2.0.0", "i": "^0.3.7", "input-otp": "1.4.2", + "ioredis": "^5.6.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", @@ -76,6 +77,7 @@ "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", + "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -2008,6 +2010,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2050,7 +2058,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -6302,7 +6309,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -6449,6 +6455,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -6932,6 +6947,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8811,7 +8835,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9099,6 +9122,30 @@ "tslib": "^2.8.0" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9559,7 +9606,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -10066,12 +10112,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10423,7 +10481,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10436,7 +10493,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -14414,6 +14470,18 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-redis": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.1.tgz", + "integrity": "sha512-JsUsVmRVI6G/XrlYtfGV1NMCbGS/CVYayHkxD5Ism5FaL8qpFHCXbFkUeIi5WJ/onJOKWCgtB/xtCLa6qSXb4g==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express-rate-limit": ">= 6" + } + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -14760,6 +14828,27 @@ "node": ">=0.8.8" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15539,6 +15628,12 @@ "node": "*" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -15926,7 +16021,6 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16573,7 +16667,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16879,7 +16972,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" From 01ef809fd390bc07569c46c57827bcd068cf3a47 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:35 -0700 Subject: [PATCH 024/219] New translations en-us.json (French) --- messages/fr-FR.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index ccf9ccea..4d23e073 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Supprimer une politique d'organisation IDP", "actionListIdpOrgs": "Lister les organisations IDP", "actionUpdateIdpOrg": "Mettre à jour une organisation IDP", + "actionCreateClient": "Créer un client", + "actionDeleteClient": "Supprimer le client", + "actionUpdateClient": "Mettre à jour le client", + "actionListClients": "Liste des clients", + "actionGetClient": "Obtenir le client", "noneSelected": "Aucune sélection", "orgNotFound2": "Aucune organisation trouvée.", "searchProgress": "Rechercher...", From 915d561286a7701ce59a34e5c739f1d2c0b9e1c4 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:36 -0700 Subject: [PATCH 025/219] New translations en-us.json (Spanish) --- messages/es-ES.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/es-ES.json b/messages/es-ES.json index e1d289c8..5bd43502 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Eliminar política de IDP Org", "actionListIdpOrgs": "Listar Orgs IDP", "actionUpdateIdpOrg": "Actualizar IDP Org", + "actionCreateClient": "Crear cliente", + "actionDeleteClient": "Eliminar cliente", + "actionUpdateClient": "Actualizar cliente", + "actionListClients": "Listar clientes", + "actionGetClient": "Obtener cliente", "noneSelected": "Ninguno seleccionado", "orgNotFound2": "No se encontraron organizaciones.", "searchProgress": "Buscar...", From 917f7520810d0d6b75d6b779db6d5cc3a5fdf701 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:37 -0700 Subject: [PATCH 026/219] New translations en-us.json (Czech) --- messages/cs-CZ.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 043367f9..b2152580 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Delete IDP Org Policy", "actionListIdpOrgs": "List IDP Orgs", "actionUpdateIdpOrg": "Update IDP Org", + "actionCreateClient": "Create Client", + "actionDeleteClient": "Delete Client", + "actionUpdateClient": "Update Client", + "actionListClients": "List Clients", + "actionGetClient": "Get Client", "noneSelected": "None selected", "orgNotFound2": "No organizations found.", "searchProgress": "Search...", From b4bde6660ab7f9952eef93e1c4b40f673f1f5664 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:38 -0700 Subject: [PATCH 027/219] New translations en-us.json (German) --- messages/de-DE.json | 129 +++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index 7cce81e5..50aac219 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1,5 +1,5 @@ { - "setupCreate": "Erstelle eine Organisation, Site und Ressourcen", + "setupCreate": "Erstelle eine Organisation, einen Standort und Ressourcen", "setupNewOrg": "Neue Organisation", "setupCreateOrg": "Organisation erstellen", "setupCreateResources": "Ressource erstellen", @@ -16,7 +16,7 @@ "componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.", "componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", "dismiss": "Verwerfen", - "componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Sites, die das Lizenzlimit der {maxSites} Sites überschreiten. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", + "componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", "componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!", "inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.", "inviteErrorUser": "Es tut uns leid, aber es scheint, als sei die Einladung, auf die du zugreifen möchtest, nicht für diesen Benutzer bestimmt.", @@ -38,25 +38,25 @@ "name": "Name", "online": "Online", "offline": "Offline", - "site": "Seite", + "site": "Standort", "dataIn": "Daten eingehend", "dataOut": "Daten ausgehend", "connectionType": "Verbindungstyp", "tunnelType": "Tunneltyp", "local": "Lokal", "edit": "Bearbeiten", - "siteConfirmDelete": "Site löschen bestätigen", - "siteDelete": "Site löschen", - "siteMessageRemove": "Sobald diese Seite entfernt ist, wird sie nicht mehr zugänglich sein. Alle Ressourcen und Ziele, die mit der Site verbunden sind, werden ebenfalls entfernt.", - "siteMessageConfirm": "Um zu bestätigen, gib den Namen der Site ein.", - "siteQuestionRemove": "Bist du sicher, dass Sie die Site {selectedSite} aus der Organisation entfernt werden soll?", - "siteManageSites": "Sites verwalten", + "siteConfirmDelete": "Standort löschen bestätigen", + "siteDelete": "Standort löschen", + "siteMessageRemove": "Sobald dieser Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle Ressourcen und Ziele, die mit diesem Standort verbunden sind, werden ebenfalls entfernt.", + "siteMessageConfirm": "Um zu bestätigen, gib den Namen des Standortes unten ein.", + "siteQuestionRemove": "Bist du sicher, dass der Standort {selectedSite} aus der Organisation entfernt werden soll?", + "siteManageSites": "Standorte verwalten", "siteDescription": "Verbindung zum Netzwerk durch sichere Tunnel erlauben", - "siteCreate": "Site erstellen", - "siteCreateDescription2": "Folge den nachfolgenden Schritten, um eine neue Site zu erstellen und zu verbinden", - "siteCreateDescription": "Erstelle eine neue Site, um Ressourcen zu verbinden", + "siteCreate": "Standort erstellen", + "siteCreateDescription2": "Folge den nachfolgenden Schritten, um einen neuen Standort zu erstellen und zu verbinden", + "siteCreateDescription": "Erstelle einen neuen Standort, um Ressourcen zu verbinden", "close": "Schließen", - "siteErrorCreate": "Fehler beim Erstellen der Site", + "siteErrorCreate": "Fehler beim Erstellen des Standortes", "siteErrorCreateKeyPair": "Schlüsselpaar oder Standardwerte nicht gefunden", "siteErrorCreateDefaults": "Standardwerte der Site nicht gefunden", "method": "Methode", @@ -70,8 +70,8 @@ "dockerRun": "Docker Run", "siteLearnLocal": "Mehr Infos zu lokalen Sites", "siteConfirmCopy": "Ich habe die Konfiguration kopiert", - "searchSitesProgress": "Sites durchsuchen...", - "siteAdd": "Site hinzufügen", + "searchSitesProgress": "Standorte durchsuchen...", + "siteAdd": "Standort hinzufügen", "siteInstallNewt": "Newt installieren", "siteInstallNewtDescription": "Installiere Newt auf deinem System.", "WgConfiguration": "WireGuard Konfiguration", @@ -82,26 +82,26 @@ "siteNewtDescription": "Nutze Newt für die beste Benutzererfahrung. Newt verwendet WireGuard as Basis und erlaubt Ihnen, Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk aus dem Pangolin-Dashboard heraus zu adressieren.", "siteRunsInDocker": "Läuft in Docker", "siteRunsInShell": "Läuft in der Konsole auf macOS, Linux und Windows", - "siteErrorDelete": "Fehler beim Löschen der Site", - "siteErrorUpdate": "Fehler beim Aktualisieren der Site", - "siteErrorUpdateDescription": "Beim Aktualisieren der Site ist ein Fehler aufgetreten.", - "siteUpdated": "Site aktualisiert", - "siteUpdatedDescription": "Die Site wurde aktualisiert.", - "siteGeneralDescription": "Allgemeine Einstellungen für diese Site konfigurieren", - "siteSettingDescription": "Konfigurieren der Site Einstellungen", + "siteErrorDelete": "Fehler beim Löschen des Standortes", + "siteErrorUpdate": "Fehler beim Aktualisieren des Standortes", + "siteErrorUpdateDescription": "Beim Aktualisieren des Standortes ist ein Fehler aufgetreten.", + "siteUpdated": "Standort aktualisiert", + "siteUpdatedDescription": "Der Standort wurde aktualisiert.", + "siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren", + "siteSettingDescription": "Konfigurieren der Standort Einstellungen", "siteSetting": "{siteName} Einstellungen", "siteNewtTunnel": "Newt-Tunnel (empfohlen)", "siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.", "siteWg": "Einfacher WireGuard Tunnel", "siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.", "siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.", - "siteSeeAll": "Alle Sites anzeigen", - "siteTunnelDescription": "Lege fest, wie du dich mit deiner Site verbinden möchtest", + "siteSeeAll": "Alle Standorte anzeigen", + "siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest", "siteNewtCredentials": "Neue Newt Zugangsdaten", "siteNewtCredentialsDescription": "So wird sich Newt mit dem Server authentifizieren", "siteCredentialsSave": "Ihre Zugangsdaten speichern", "siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.", - "siteInfo": "Site-Informationen", + "siteInfo": "Standort-Informationen", "status": "Status", "shareTitle": "Links zum Teilen verwalten", "shareDescription": "Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren", @@ -163,10 +163,10 @@ "resourceSeeAll": "Alle Ressourcen anzeigen", "resourceInfo": "Ressourcen-Informationen", "resourceNameDescription": "Dies ist der Anzeigename für die Ressource.", - "siteSelect": "Site auswählen", - "siteSearch": "Website durchsuchen", - "siteNotFound": "Keine Site gefunden.", - "siteSelectionDescription": "Diese Seite wird die Verbindung zu der Ressource herstellen.", + "siteSelect": "Standort auswählen", + "siteSearch": "Standorte durchsuchen", + "siteNotFound": "Keinen Standort gefunden.", + "siteSelectionDescription": "Dieser Standort wird die Verbindung zu der Ressource herstellen.", "resourceType": "Ressourcentyp", "resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten", "resourceHTTPSSettings": "HTTPS-Einstellungen", @@ -302,7 +302,7 @@ "userQuestionRemove": "Sind Sie sicher, dass Sie {selectedUser} dauerhaft vom Server löschen möchten?", "licenseKey": "Lizenzschlüssel", "valid": "Gültig", - "numberOfSites": "Anzahl der Sites", + "numberOfSites": "Anzahl der Standorte", "licenseKeySearch": "Lizenzschlüssel suchen...", "licenseKeyAdd": "Lizenzschlüssel hinzufügen", "type": "Typ", @@ -342,16 +342,16 @@ "licensedNot": "Nicht lizenziert", "hostId": "Host-ID", "licenseReckeckAll": "Überprüfe alle Schlüssel", - "licenseSiteUsage": "Website-Nutzung", - "licenseSiteUsageDecsription": "Sehen Sie sich die Anzahl der Sites an, die diese Lizenz verwenden.", - "licenseNoSiteLimit": "Die Anzahl der Sites, die einen nicht lizenzierten Host verwenden, ist unbegrenzt.", + "licenseSiteUsage": "Standort-Nutzung", + "licenseSiteUsageDecsription": "Sehen Sie sich die Anzahl der Standorte an, die diese Lizenz verwenden.", + "licenseNoSiteLimit": "Die Anzahl der Standorte, die einen nicht lizenzierten Host verwenden, ist unbegrenzt.", "licensePurchase": "Lizenz kaufen", - "licensePurchaseSites": "Zusätzliche Seiten kaufen", - "licenseSitesUsedMax": "{usedSites} der {maxSites} Seiten verwendet", - "licenseSitesUsed": "{count, plural, =0 {# Seiten} one {# Seite} other {# Seiten}} im System.", + "licensePurchaseSites": "Zusätzliche Standorte kaufen\n", + "licenseSitesUsedMax": "{usedSites} von {maxSites} Standorten verwendet", + "licenseSitesUsed": "{count, plural, =0 {# Standorte} one {# Standort} other {# Standorte}} im System.", "licensePurchaseDescription": "Wähle aus, für wieviele Seiten du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Seiten hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}", "licenseFee": "Lizenzgebühr", - "licensePriceSite": "Preis pro Seite", + "licensePriceSite": "Preis pro Standort", "total": "Gesamt", "licenseContinuePayment": "Weiter zur Zahlung", "pricingPage": "Preisseite", @@ -467,7 +467,7 @@ "targetErrorDuplicate": "Doppeltes Ziel", "targetErrorDuplicateDescription": "Ein Ziel mit diesen Einstellungen existiert bereits", "targetWireGuardErrorInvalidIp": "Ungültige Ziel-IP", - "targetWireGuardErrorInvalidIpDescription": "Die Ziel-IP muss innerhalb des Site-Subnets liegen", + "targetWireGuardErrorInvalidIpDescription": "Die Ziel-IP muss innerhalb des Standort-Subnets liegen", "targetsUpdated": "Ziele aktualisiert", "targetsUpdatedDescription": "Ziele und Einstellungen erfolgreich aktualisiert", "targetsErrorUpdate": "Fehler beim Aktualisieren der Ziele", @@ -558,8 +558,8 @@ "resourceErrorCreateDescription": "Beim Erstellen der Ressource ist ein Fehler aufgetreten", "resourceErrorCreateMessage": "Fehler beim Erstellen der Ressource:", "resourceErrorCreateMessageDescription": "Ein unerwarteter Fehler ist aufgetreten", - "sitesErrorFetch": "Fehler beim Abrufen der Sites", - "sitesErrorFetchDescription": "Beim Abrufen der Sites ist ein Fehler aufgetreten", + "sitesErrorFetch": "Fehler beim Abrufen der Standorte", + "sitesErrorFetchDescription": "Beim Abrufen der Standorte ist ein Fehler aufgetreten", "domainsErrorFetch": "Fehler beim Abrufen der Domains", "domainsErrorFetchDescription": "Beim Abrufen der Domains ist ein Fehler aufgetreten", "none": "Keine", @@ -677,10 +677,10 @@ "resourceGeneralDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diese Ressource", "resourceEnable": "Ressource aktivieren", "resourceTransfer": "Ressource übertragen", - "resourceTransferDescription": "Diese Ressource auf eine andere Site übertragen", + "resourceTransferDescription": "Diese Ressource auf einen anderen Standort übertragen", "resourceTransferSubmit": "Ressource übertragen", - "siteDestination": "Zielsite", - "searchSites": "Sites durchsuchen", + "siteDestination": "Zielort", + "searchSites": "Standorte durchsuchen", "accessRoleCreate": "Rolle erstellen", "accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.", "accessRoleCreateSubmit": "Rolle erstellen", @@ -700,7 +700,7 @@ "accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.", "accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.", "manage": "Verwalten", - "sitesNotFound": "Keine Sites gefunden.", + "sitesNotFound": "Keine Standorte gefunden.", "pangolinServerAdmin": "Server-Admin - Pangolin", "licenseTierProfessional": "Professional Lizenz", "licenseTierEnterprise": "Enterprise Lizenz", @@ -708,10 +708,10 @@ "licensed": "Lizenziert", "yes": "Ja", "no": "Nein", - "sitesAdditional": "Zusätzliche Sites", + "sitesAdditional": "Zusätzliche Standorte", "licenseKeys": "Lizenzschlüssel", - "sitestCountDecrease": "Anzahl der Sites verringern", - "sitestCountIncrease": "Anzahl der Sites erhöhen", + "sitestCountDecrease": "Anzahl der Standorte verringern", + "sitestCountIncrease": "Anzahl der Standorte erhöhen", "idpManage": "Identitätsanbieter verwalten", "idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten", "idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht", @@ -963,12 +963,12 @@ "actionGetUser": "Benutzer abrufen", "actionGetOrgUser": "Organisationsbenutzer abrufen", "actionListOrgDomains": "Organisationsdomänen auflisten", - "actionCreateSite": "Site erstellen", - "actionDeleteSite": "Site löschen", - "actionGetSite": "Site abrufen", - "actionListSites": "Sites auflisten", - "actionUpdateSite": "Site aktualisieren", - "actionListSiteRoles": "Erlaubte Site-Rollen auflisten", + "actionCreateSite": "Standort erstellen", + "actionDeleteSite": "Standort löschen", + "actionGetSite": "Standort abrufen", + "actionListSites": "Standorte auflisten", + "actionUpdateSite": "Standorte aktualisieren", + "actionListSiteRoles": "Erlaubte Standort-Rollen auflisten", "actionCreateResource": "Ressource erstellen", "actionDeleteResource": "Ressource löschen", "actionGetResource": "Ressource abrufen", @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen", "actionListIdpOrgs": "IDP-Organisationen auflisten", "actionUpdateIdpOrg": "IDP-Organisation aktualisieren", + "actionCreateClient": "Kunde erstellen", + "actionDeleteClient": "Kunde löschen", + "actionUpdateClient": "Kunde aktualisieren", + "actionListClients": "Kunden auflisten", + "actionGetClient": "Kunde holen", "noneSelected": "Keine ausgewählt", "orgNotFound2": "Keine Organisationen gefunden.", "searchProgress": "Suche...", @@ -1073,7 +1078,7 @@ "language": "Sprache", "verificationCodeRequired": "Code ist erforderlich", "userErrorNoUpdate": "Kein Benutzer zum Aktualisieren", - "siteErrorNoUpdate": "Keine Site zum Aktualisieren", + "siteErrorNoUpdate": "Keine Standorte zum Aktualisieren", "resourceErrorNoUpdate": "Keine Ressource zum Aktualisieren", "authErrorNoUpdate": "Keine Auth-Informationen zum Aktualisieren", "orgErrorNoUpdate": "Keine Organisation zum Aktualisieren", @@ -1081,7 +1086,7 @@ "apiKeysErrorNoUpdate": "Kein API-Schlüssel zum Aktualisieren", "sidebarOverview": "Übersicht", "sidebarHome": "Zuhause", - "sidebarSites": "Seiten", + "sidebarSites": "Standorte", "sidebarResources": "Ressourcen", "sidebarAccessControl": "Zugriffskontrolle", "sidebarUsers": "Benutzer", @@ -1280,21 +1285,21 @@ "and": "und", "privacyPolicy": "Datenschutzrichtlinie" }, - "siteRequired": "Site ist erforderlich.", + "siteRequired": "Standort ist erforderlich.", "olmTunnel": "Olm Tunnel", "olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung", "errorCreatingClient": "Fehler beim Erstellen des Clients", "clientDefaultsNotFound": "Kundenvorgaben nicht gefunden", "createClient": "Client erstellen", - "createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Sites.", + "createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.", "seeAllClients": "Alle Clients anzeigen", "clientInformation": "Kundeninformationen", "clientNamePlaceholder": "Kundenname", "address": "Adresse", "subnetPlaceholder": "Subnetz", "addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.", - "selectSites": "Sites auswählen", - "sitesDescription": "Der Client wird zu den ausgewählten Sites eine Verbindung haben.", + "selectSites": "Standorte auswählen", + "sitesDescription": "Der Client wird zu den ausgewählten Standorten eine Verbindung haben.", "clientInstallOlm": "Olm installieren", "clientInstallOlmDescription": "Olm auf Ihrem System zum Laufen bringen", "clientOlmCredentials": "Olm-Zugangsdaten", @@ -1309,13 +1314,13 @@ "clientUpdatedDescription": "Der Client wurde aktualisiert.", "clientUpdateFailed": "Fehler beim Aktualisieren des Clients", "clientUpdateError": "Beim Aktualisieren des Clients ist ein Fehler aufgetreten.", - "sitesFetchFailed": "Fehler beim Abrufen von Sites", - "sitesFetchError": "Beim Abrufen von Sites ist ein Fehler aufgetreten.", + "sitesFetchFailed": "Fehler beim Abrufen von Standorten", + "sitesFetchError": "Beim Abrufen von Standorten ist ein Fehler aufgetreten.", "olmErrorFetchReleases": "Beim Abrufen von Olm-Veröffentlichungen ist ein Fehler aufgetreten.", "olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.", "remoteSubnets": "Remote-Subnetze", "enterCidrRange": "Geben Sie den CIDR-Bereich ein", - "remoteSubnetsDescription": "Fügen Sie CIDR-Bereiche hinzu, die aus der Ferne auf diese Site zugreifen können. Verwenden Sie das Format wie 10.0.0.0/24 oder 192.168.1.0/24.", + "remoteSubnetsDescription": "Fügen Sie CIDR-Bereiche hinzu, die aus der Ferne auf diesen Standort zugreifen können. Verwenden Sie das Format wie 10.0.0.0/24 oder 192.168.1.0/24.", "resourceEnableProxy": "Öffentlichen Proxy aktivieren", "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", "externalProxyEnabled": "Externer Proxy aktiviert" From 3e333769bb5c41f209d546c3d2299c9ab94a92ff Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:39 -0700 Subject: [PATCH 028/219] New translations en-us.json (Italian) --- messages/it-IT.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/it-IT.json b/messages/it-IT.json index 00fff828..d336011a 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Elimina Politica Org IDP", "actionListIdpOrgs": "Elenca Org IDP", "actionUpdateIdpOrg": "Aggiorna Org IDP", + "actionCreateClient": "Crea Client", + "actionDeleteClient": "Elimina Client", + "actionUpdateClient": "Aggiorna Client", + "actionListClients": "Elenco Clienti", + "actionGetClient": "Ottieni Client", "noneSelected": "Nessuna selezione", "orgNotFound2": "Nessuna organizzazione trovata.", "searchProgress": "Ricerca...", From 91bac29ea34f29bfde0a7f9c2ad89a2d6de0f899 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:40 -0700 Subject: [PATCH 029/219] New translations en-us.json (Korean) --- messages/ko-KR.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 923cf4b3..c70d34ff 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "IDP 조직 정책 삭제", "actionListIdpOrgs": "IDP 조직 목록", "actionUpdateIdpOrg": "IDP 조직 업데이트", + "actionCreateClient": "Create Client", + "actionDeleteClient": "Delete Client", + "actionUpdateClient": "Update Client", + "actionListClients": "List Clients", + "actionGetClient": "Get Client", "noneSelected": "선택된 항목 없음", "orgNotFound2": "조직이 없습니다.", "searchProgress": "검색...", From 8e0a8dc272a941abc5004776a9dedcca19a4e62a Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:41 -0700 Subject: [PATCH 030/219] New translations en-us.json (Dutch) --- messages/nl-NL.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 14cfe9ac..38f68a3b 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Verwijder IDP Org Beleid", "actionListIdpOrgs": "Toon IDP Orgs", "actionUpdateIdpOrg": "IDP-org bijwerken", + "actionCreateClient": "Client aanmaken", + "actionDeleteClient": "Verwijder klant", + "actionUpdateClient": "Klant bijwerken", + "actionListClients": "Lijst klanten", + "actionGetClient": "Client ophalen", "noneSelected": "Niet geselecteerd", "orgNotFound2": "Geen organisaties gevonden.", "searchProgress": "Zoeken...", From 917e7a8c1d00961cf94a672d9cc3153e7f788887 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:43 -0700 Subject: [PATCH 031/219] New translations en-us.json (Polish) --- messages/pl-PL.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 087a717e..0f1eb29a 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Usuń politykę organizacji IDP", "actionListIdpOrgs": "Lista organizacji IDP", "actionUpdateIdpOrg": "Aktualizuj organizację IDP", + "actionCreateClient": "Utwórz klienta", + "actionDeleteClient": "Usuń klienta", + "actionUpdateClient": "Aktualizuj klienta", + "actionListClients": "Lista klientów", + "actionGetClient": "Pobierz klienta", "noneSelected": "Nie wybrano", "orgNotFound2": "Nie znaleziono organizacji.", "searchProgress": "Szukaj...", From ec21153d4b499c7eed7a1c8924fbd031506e8dee Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:44 -0700 Subject: [PATCH 032/219] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 5e46c51f..9a3104bd 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Eliminar Política de Organização IDP", "actionListIdpOrgs": "Listar Organizações IDP", "actionUpdateIdpOrg": "Atualizar Organização IDP", + "actionCreateClient": "Criar Cliente", + "actionDeleteClient": "Excluir Cliente", + "actionUpdateClient": "Atualizar Cliente", + "actionListClients": "Listar Clientes", + "actionGetClient": "Obter Cliente", "noneSelected": "Nenhum selecionado", "orgNotFound2": "Nenhuma organização encontrada.", "searchProgress": "Pesquisar...", From e3287a7e9f96bf32231f36ff5db13da67a0992c4 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:45 -0700 Subject: [PATCH 033/219] New translations en-us.json (Turkish) --- messages/tr-TR.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index abd4b3e9..e4d68eba 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Kimlik Sağlayıcı Organizasyon Politikasını Sil", "actionListIdpOrgs": "Kimlik Sağlayıcı Organizasyonları Listele", "actionUpdateIdpOrg": "Kimlik Sağlayıcı Organizasyonu Güncelle", + "actionCreateClient": "Müşteri Oluştur", + "actionDeleteClient": "Müşteri Sil", + "actionUpdateClient": "Müşteri Güncelle", + "actionListClients": "Müşterileri Listele", + "actionGetClient": "Müşteriyi Al", "noneSelected": "Hiçbiri seçili değil", "orgNotFound2": "Hiçbir organizasyon bulunamadı.", "searchProgress": "Ara...", From f31e4e3176349e5eecd10d30246e5f5d7f5b1a90 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:46 -0700 Subject: [PATCH 034/219] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 38da8715..b18a7ab7 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "删除 IDP组织策略", "actionListIdpOrgs": "列出 IDP组织", "actionUpdateIdpOrg": "更新 IDP组织", + "actionCreateClient": "创建客户端", + "actionDeleteClient": "删除客户端", + "actionUpdateClient": "更新客户端", + "actionListClients": "列出客户端", + "actionGetClient": "获取客户端", "noneSelected": "未选择", "orgNotFound2": "未找到组织。", "searchProgress": "搜索中...", From 1ea9fd2d49bbb2feb74f2c757563baa62a6561d7 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:47 -0700 Subject: [PATCH 035/219] New translations en-us.json (Russian) --- messages/ru-RU.json | 209 +++++++++++++++++++++++--------------------- 1 file changed, 107 insertions(+), 102 deletions(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 5bbbf780..90d3804d 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -925,74 +925,74 @@ "supportKeyInvalid": "Недействительный ключ", "supportKeyInvalidDescription": "Ваш ключ поддержки недействителен.", "supportKeyValid": "Действительный ключ", - "supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!", - "supportKeyErrorValidationDescription": "Failed to validate supporter key.", - "supportKey": "Support Development and Adopt a Pangolin!", + "supportKeyValidDescription": "Ваш ключ поддержки был проверен. Спасибо за поддержку!", + "supportKeyErrorValidationDescription": "Не удалось проверить ключ поддержки.", + "supportKey": "Поддержите разработку и усыновите Панголина!", "supportKeyDescription": "Приобретите ключ поддержки, чтобы помочь нам продолжать разработку Pangolin для сообщества. Ваш вклад позволяет нам уделять больше времени поддержке и добавлению новых функций в приложение для всех. Мы никогда не будем использовать это для платного доступа к функциям. Это отдельно от любой коммерческой версии.", - "supportKeyPet": "You will also get to adopt and meet your very own pet Pangolin!", - "supportKeyPurchase": "Payments are processed via GitHub. Afterward, you can retrieve your key on", - "supportKeyPurchaseLink": "our website", - "supportKeyPurchase2": "and redeem it here.", - "supportKeyLearnMore": "Learn more.", - "supportKeyOptions": "Please select the option that best suits you.", - "supportKetOptionFull": "Full Supporter", - "forWholeServer": "For the whole server", - "lifetimePurchase": "Lifetime purchase", - "supporterStatus": "Supporter status", - "buy": "Buy", - "supportKeyOptionLimited": "Limited Supporter", - "forFiveUsers": "For 5 or less users", - "supportKeyRedeem": "Redeem Supporter Key", - "supportKeyHideSevenDays": "Hide for 7 days", - "supportKeyEnter": "Enter Supporter Key", - "supportKeyEnterDescription": "Meet your very own pet Pangolin!", - "githubUsername": "GitHub Username", - "supportKeyInput": "Supporter Key", - "supportKeyBuy": "Buy Supporter Key", - "logoutError": "Error logging out", - "signingAs": "Signed in as", - "serverAdmin": "Server Admin", - "otpEnable": "Enable Two-factor", - "otpDisable": "Disable Two-factor", - "logout": "Log Out", - "licenseTierProfessionalRequired": "Professional Edition Required", + "supportKeyPet": "Вы также сможете усыновить и встретить вашего собственного питомца Панголина!", + "supportKeyPurchase": "Платежи обрабатываются через GitHub. После этого вы сможете получить свой ключ на", + "supportKeyPurchaseLink": "нашем сайте", + "supportKeyPurchase2": "и активировать его здесь.", + "supportKeyLearnMore": "Узнать больше.", + "supportKeyOptions": "Пожалуйста, выберите подходящий вам вариант.", + "supportKetOptionFull": "Полная поддержка", + "forWholeServer": "За весь сервер", + "lifetimePurchase": "Пожизненная покупка", + "supporterStatus": "Статус поддержки", + "buy": "Купить", + "supportKeyOptionLimited": "Лимитированная поддержка", + "forFiveUsers": "За 5 или меньше пользователей", + "supportKeyRedeem": "Использовать ключ Поддержки", + "supportKeyHideSevenDays": "Скрыть на 7 дней", + "supportKeyEnter": "Введите ключ поддержки", + "supportKeyEnterDescription": "Встречайте своего питомца Панголина!", + "githubUsername": "Имя пользователя Github", + "supportKeyInput": "Ключ поддержки", + "supportKeyBuy": "Ключ поддержки", + "logoutError": "Ошибка при выходе", + "signingAs": "Вы вошли как", + "serverAdmin": "Администратор сервера", + "otpEnable": "Включить Двухфакторную Аутентификацию", + "otpDisable": "Отключить двухфакторную аутентификацию", + "logout": "Выйти", + "licenseTierProfessionalRequired": "Требуется профессиональная версия", "licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.", - "actionGetOrg": "Get Organization", - "actionUpdateOrg": "Update Organization", - "actionUpdateUser": "Update User", - "actionGetUser": "Get User", - "actionGetOrgUser": "Get Organization User", - "actionListOrgDomains": "List Organization Domains", - "actionCreateSite": "Create Site", - "actionDeleteSite": "Delete Site", - "actionGetSite": "Get Site", - "actionListSites": "List Sites", - "actionUpdateSite": "Update Site", - "actionListSiteRoles": "List Allowed Site Roles", - "actionCreateResource": "Create Resource", - "actionDeleteResource": "Delete Resource", - "actionGetResource": "Get Resource", - "actionListResource": "List Resources", - "actionUpdateResource": "Update Resource", - "actionListResourceUsers": "List Resource Users", - "actionSetResourceUsers": "Set Resource Users", - "actionSetAllowedResourceRoles": "Set Allowed Resource Roles", - "actionListAllowedResourceRoles": "List Allowed Resource Roles", - "actionSetResourcePassword": "Set Resource Password", - "actionSetResourcePincode": "Set Resource Pincode", + "actionGetOrg": "Получить организацию", + "actionUpdateOrg": "Обновить организацию", + "actionUpdateUser": "Обновить пользователя", + "actionGetUser": "Получить пользователя", + "actionGetOrgUser": "Получить пользователя организации", + "actionListOrgDomains": "Список доменов организации", + "actionCreateSite": "Создать сайт", + "actionDeleteSite": "Удалить сайт", + "actionGetSite": "Получить сайт", + "actionListSites": "Список сайтов", + "actionUpdateSite": "Обновить сайт", + "actionListSiteRoles": "Список разрешенных ролей сайта", + "actionCreateResource": "Создать ресурс", + "actionDeleteResource": "Удалить ресурс", + "actionGetResource": "Получить ресурсы", + "actionListResource": "Список ресурсов", + "actionUpdateResource": "Обновить ресурс", + "actionListResourceUsers": "Список пользователей ресурсов", + "actionSetResourceUsers": "Список пользователей ресурсов", + "actionSetAllowedResourceRoles": "Набор разрешенных ролей ресурсов", + "actionListAllowedResourceRoles": "Список разрешенных ролей сайта", + "actionSetResourcePassword": "Задать пароль ресурса", + "actionSetResourcePincode": "Установить ПИН-код ресурса", "actionSetResourceEmailWhitelist": "Set Resource Email Whitelist", "actionGetResourceEmailWhitelist": "Get Resource Email Whitelist", - "actionCreateTarget": "Create Target", - "actionDeleteTarget": "Delete Target", - "actionGetTarget": "Get Target", - "actionListTargets": "List Targets", - "actionUpdateTarget": "Update Target", - "actionCreateRole": "Create Role", - "actionDeleteRole": "Delete Role", - "actionGetRole": "Get Role", - "actionListRole": "List Roles", - "actionUpdateRole": "Update Role", - "actionListAllowedRoleResources": "List Allowed Role Resources", + "actionCreateTarget": "Создать цель", + "actionDeleteTarget": "Удалить цель", + "actionGetTarget": "Получить цель", + "actionListTargets": "Список целей", + "actionUpdateTarget": "Обновить цель", + "actionCreateRole": "Создать роль", + "actionDeleteRole": "Удалить роль", + "actionGetRole": "Получить Роль", + "actionListRole": "Список ролей", + "actionUpdateRole": "Обновить роль", + "actionListAllowedRoleResources": "Список разрешенных ролей сайта", "actionInviteUser": "Пригласить пользователя", "actionRemoveUser": "Удалить пользователя", "actionListUsers": "Список пользователей", @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Удалить политику IDP организации", "actionListIdpOrgs": "Список организаций IDP", "actionUpdateIdpOrg": "Обновить организацию IDP", + "actionCreateClient": "Создать Клиента", + "actionDeleteClient": "Удалить Клиента", + "actionUpdateClient": "Обновить Клиента", + "actionListClients": "Список Клиентов", + "actionGetClient": "Получить Клиента", "noneSelected": "Ничего не выбрано", "orgNotFound2": "Организации не найдены.", "searchProgress": "Поиск...", @@ -1093,8 +1098,8 @@ "sidebarAllUsers": "Все пользователи", "sidebarIdentityProviders": "Поставщики удостоверений", "sidebarLicense": "Лицензия", - "sidebarClients": "Clients (Beta)", - "sidebarDomains": "Domains", + "sidebarClients": "Клиенты (бета)", + "sidebarDomains": "Домены", "enableDockerSocket": "Включить Docker Socket", "enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.", "enableDockerSocketLink": "Узнать больше", @@ -1134,36 +1139,36 @@ "dark": "тёмная", "system": "системная", "theme": "Тема", - "subnetRequired": "Subnet is required", + "subnetRequired": "Требуется подсеть", "initialSetupTitle": "Начальная настройка сервера", "initialSetupDescription": "Создайте первоначальную учётную запись администратора сервера. Может существовать только один администратор сервера. Вы всегда можете изменить эти учётные данные позже.", "createAdminAccount": "Создать учётную запись администратора", "setupErrorCreateAdmin": "Произошла ошибка при создании учётной записи администратора сервера.", - "certificateStatus": "Certificate Status", - "loading": "Loading", - "restart": "Restart", - "domains": "Domains", - "domainsDescription": "Manage domains for your organization", - "domainsSearch": "Search domains...", - "domainAdd": "Add Domain", - "domainAddDescription": "Register a new domain with your organization", - "domainCreate": "Create Domain", - "domainCreatedDescription": "Domain created successfully", - "domainDeletedDescription": "Domain deleted successfully", - "domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?", - "domainMessageRemove": "Once removed, the domain will no longer be associated with your account.", - "domainMessageConfirm": "To confirm, please type the domain name below.", - "domainConfirmDelete": "Confirm Delete Domain", - "domainDelete": "Delete Domain", - "domain": "Domain", - "selectDomainTypeNsName": "Domain Delegation (NS)", - "selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.", - "selectDomainTypeCnameName": "Single Domain (CNAME)", - "selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.", + "certificateStatus": "Статус сертификата", + "loading": "Загрузка", + "restart": "Перезагрузка", + "domains": "Домены", + "domainsDescription": "Управление доменами для вашей организации", + "domainsSearch": "Поиск доменов...", + "domainAdd": "Добавить Домен", + "domainAddDescription": "Зарегистрировать новый домен в вашей организации", + "domainCreate": "Создать Домен", + "domainCreatedDescription": "Домен успешно создан", + "domainDeletedDescription": "Домен успешно удален", + "domainQuestionRemove": "Вы уверены, что хотите удалить домен {domain} из вашего аккаунта?", + "domainMessageRemove": "После удаления домен больше не будет связан с вашей учетной записью.", + "domainMessageConfirm": "Для подтверждения введите ниже имя домена.", + "domainConfirmDelete": "Подтвердить удаление домена", + "domainDelete": "Удалить Домен", + "domain": "Домен", + "selectDomainTypeNsName": "Делегация домена (NS)", + "selectDomainTypeNsDescription": "Этот домен и все его субдомены. Используйте это, когда вы хотите управлять всей доменной зоной.", + "selectDomainTypeCnameName": "Одиночный домен (CNAME)", + "selectDomainTypeCnameDescription": "Только этот конкретный домен. Используйте это для отдельных субдоменов или отдельных записей домена.", "selectDomainTypeWildcardName": "Wildcard Domain", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", - "domainDelegation": "Single Domain", - "selectType": "Select a type", + "selectDomainTypeWildcardDescription": "Этот домен и его субдомены.", + "domainDelegation": "Единый домен", + "selectType": "Выберите тип", "actions": "Actions", "refresh": "Refresh", "refreshError": "Failed to refresh data", @@ -1268,19 +1273,19 @@ "createDomainARecords": "A Records", "createDomainRecordNumber": "Record {number}", "createDomainTxtRecords": "TXT Records", - "createDomainSaveTheseRecords": "Save These Records", - "createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.", - "createDomainDnsPropagation": "DNS Propagation", - "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", - "resourcePortRequired": "Port number is required for non-HTTP resources", - "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "createDomainSaveTheseRecords": "Сохранить эти записи", + "createDomainSaveTheseRecordsDescription": "Обязательно сохраните эти DNS записи, так как вы их больше не увидите.", + "createDomainDnsPropagation": "Распространение DNS", + "createDomainDnsPropagationDescription": "Изменения DNS могут занять некоторое время для распространения через интернет. Это может занять от нескольких минут до 48 часов в зависимости от вашего DNS провайдера и настроек TTL.", + "resourcePortRequired": "Номер порта необходим для не-HTTP ресурсов", + "resourcePortNotAllowed": "Номер порта не должен быть установлен для HTTP ресурсов", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Я согласен с", + "termsOfService": "условия использования", + "and": "и", + "privacyPolicy": "политика конфиденциальности" }, - "siteRequired": "Site is required.", + "siteRequired": "Необходимо указать сайт.", "olmTunnel": "Olm Tunnel", "olmTunnelDescription": "Use Olm for client connectivity", "errorCreatingClient": "Error creating client", From b1a92fd4e0c37339d7827906d816ad4e925e5fd7 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 3 Aug 2025 11:40:48 -0700 Subject: [PATCH 036/219] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 1327 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1327 insertions(+) create mode 100644 messages/bg-BG.json diff --git a/messages/bg-BG.json b/messages/bg-BG.json new file mode 100644 index 00000000..bf786a24 --- /dev/null +++ b/messages/bg-BG.json @@ -0,0 +1,1327 @@ +{ + "setupCreate": "Създайте своя организация, сайт и ресурси", + "setupNewOrg": "Нова организация", + "setupCreateOrg": "Създай организация", + "setupCreateResources": "Създай ресурси", + "setupOrgName": "Име на организацията", + "orgDisplayName": "Това е публичното име на вашата организация.", + "orgId": "Идентификатор на организация", + "setupIdentifierMessage": "Това е уникалният идентификатор на вашата организация. Това е различно от публичното ѝ име.", + "setupErrorIdentifier": "Идентификаторът на организация вече е зает. Моля, изберете друг.", + "componentsErrorNoMemberCreate": "В момента не сте част от организация. Създайте организация, за да продължите.", + "componentsErrorNoMember": "В момента не сте част от организация.", + "welcome": "Добре дошли!", + "welcomeTo": "Добре дошли в", + "componentsCreateOrg": "Създай организация", + "componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.", + "componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.", + "dismiss": "Dismiss", + "componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.", + "componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!", + "inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.", + "inviteErrorUser": "We're sorry, but it looks like the invite you're trying to access is not for this user.", + "inviteLoginUser": "Please make sure you're logged in as the correct user.", + "inviteErrorNoUser": "We're sorry, but it looks like the invite you're trying to access is not for a user that exists.", + "inviteCreateUser": "Please create an account first.", + "goHome": "Go Home", + "inviteLogInOtherUser": "Log In as a Different User", + "createAnAccount": "Create an Account", + "inviteNotAccepted": "Invite Not Accepted", + "authCreateAccount": "Create an account to get started", + "authNoAccount": "Don't have an account?", + "email": "Email", + "password": "Password", + "confirmPassword": "Confirm Password", + "createAccount": "Create Account", + "viewSettings": "View settings", + "delete": "Delete", + "name": "Name", + "online": "Online", + "offline": "Offline", + "site": "Site", + "dataIn": "Data In", + "dataOut": "Data Out", + "connectionType": "Connection Type", + "tunnelType": "Tunnel Type", + "local": "Local", + "edit": "Edit", + "siteConfirmDelete": "Confirm Delete Site", + "siteDelete": "Delete Site", + "siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.", + "siteMessageConfirm": "To confirm, please type the name of the site below.", + "siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?", + "siteManageSites": "Manage Sites", + "siteDescription": "Allow connectivity to your network through secure tunnels", + "siteCreate": "Create Site", + "siteCreateDescription2": "Follow the steps below to create and connect a new site", + "siteCreateDescription": "Create a new site to start connecting your resources", + "close": "Close", + "siteErrorCreate": "Error creating site", + "siteErrorCreateKeyPair": "Key pair or site defaults not found", + "siteErrorCreateDefaults": "Site defaults not found", + "method": "Method", + "siteMethodDescription": "This is how you will expose connections.", + "siteLearnNewt": "Learn how to install Newt on your system", + "siteSeeConfigOnce": "You will only be able to see the configuration once.", + "siteLoadWGConfig": "Loading WireGuard configuration...", + "siteDocker": "Expand for Docker Deployment Details", + "toggle": "Toggle", + "dockerCompose": "Docker Compose", + "dockerRun": "Docker Run", + "siteLearnLocal": "Local sites do not tunnel, learn more", + "siteConfirmCopy": "I have copied the config", + "searchSitesProgress": "Search sites...", + "siteAdd": "Add Site", + "siteInstallNewt": "Install Newt", + "siteInstallNewtDescription": "Get Newt running on your system", + "WgConfiguration": "WireGuard Configuration", + "WgConfigurationDescription": "Use the following configuration to connect to your network", + "operatingSystem": "Operating System", + "commands": "Commands", + "recommended": "Recommended", + "siteNewtDescription": "For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard.", + "siteRunsInDocker": "Runs in Docker", + "siteRunsInShell": "Runs in shell on macOS, Linux, and Windows", + "siteErrorDelete": "Error deleting site", + "siteErrorUpdate": "Failed to update site", + "siteErrorUpdateDescription": "An error occurred while updating the site.", + "siteUpdated": "Site updated", + "siteUpdatedDescription": "The site has been updated.", + "siteGeneralDescription": "Configure the general settings for this site", + "siteSettingDescription": "Configure the settings on your site", + "siteSetting": "{siteName} Settings", + "siteNewtTunnel": "Newt Tunnel (Recommended)", + "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", + "siteWg": "Basic WireGuard", + "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", + "siteLocalDescription": "Local resources only. No tunneling.", + "siteSeeAll": "See All Sites", + "siteTunnelDescription": "Determine how you want to connect to your site", + "siteNewtCredentials": "Newt Credentials", + "siteNewtCredentialsDescription": "This is how Newt will authenticate with the server", + "siteCredentialsSave": "Save Your Credentials", + "siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "siteInfo": "Site Information", + "status": "Status", + "shareTitle": "Manage Share Links", + "shareDescription": "Create shareable links to grant temporary or permanent access to your resources", + "shareSearch": "Search share links...", + "shareCreate": "Create Share Link", + "shareErrorDelete": "Failed to delete link", + "shareErrorDeleteMessage": "An error occurred deleting link", + "shareDeleted": "Link deleted", + "shareDeletedDescription": "The link has been deleted", + "shareTokenDescription": "Your access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.", + "accessToken": "Access Token", + "usageExamples": "Usage Examples", + "tokenId": "Token ID", + "requestHeades": "Request Headers", + "queryParameter": "Query Parameter", + "importantNote": "Important Note", + "shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.", + "token": "Token", + "shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.", + "shareErrorFetchResource": "Failed to fetch resources", + "shareErrorFetchResourceDescription": "An error occurred while fetching the resources", + "shareErrorCreate": "Failed to create share link", + "shareErrorCreateDescription": "An error occurred while creating the share link", + "shareCreateDescription": "Anyone with this link can access the resource", + "shareTitleOptional": "Title (optional)", + "expireIn": "Expire In", + "neverExpire": "Never expire", + "shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.", + "shareSeeOnce": "You will only be able to see this linkonce. Make sure to copy it.", + "shareAccessHint": "Anyone with this link can access the resource. Share it with care.", + "shareTokenUsage": "See Access Token Usage", + "createLink": "Create Link", + "resourcesNotFound": "No resources found", + "resourceSearch": "Search resources", + "openMenu": "Open menu", + "resource": "Resource", + "title": "Title", + "created": "Created", + "expires": "Expires", + "never": "Never", + "shareErrorSelectResource": "Please select a resource", + "resourceTitle": "Manage Resources", + "resourceDescription": "Create secure proxies to your private applications", + "resourcesSearch": "Search resources...", + "resourceAdd": "Add Resource", + "resourceErrorDelte": "Error deleting resource", + "authentication": "Authentication", + "protected": "Protected", + "notProtected": "Not Protected", + "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", + "resourceMessageConfirm": "To confirm, please type the name of the resource below.", + "resourceQuestionRemove": "Are you sure you want to remove the resource {selectedResource} from the organization?", + "resourceHTTP": "HTTPS Resource", + "resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.", + "resourceRaw": "Raw TCP/UDP Resource", + "resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number.", + "resourceCreate": "Create Resource", + "resourceCreateDescription": "Follow the steps below to create a new resource", + "resourceSeeAll": "See All Resources", + "resourceInfo": "Resource Information", + "resourceNameDescription": "This is the display name for the resource.", + "siteSelect": "Select site", + "siteSearch": "Search site", + "siteNotFound": "No site found.", + "siteSelectionDescription": "This site will provide connectivity to the resource.", + "resourceType": "Resource Type", + "resourceTypeDescription": "Determine how you want to access your resource", + "resourceHTTPSSettings": "HTTPS Settings", + "resourceHTTPSSettingsDescription": "Configure how your resource will be accessed over HTTPS", + "domainType": "Domain Type", + "subdomain": "Subdomain", + "baseDomain": "Base Domain", + "subdomnainDescription": "The subdomain where your resource will be accessible.", + "resourceRawSettings": "TCP/UDP Settings", + "resourceRawSettingsDescription": "Configure how your resource will be accessed over TCP/UDP", + "protocol": "Protocol", + "protocolSelect": "Select a protocol", + "resourcePortNumber": "Port Number", + "resourcePortNumberDescription": "The external port number to proxy requests.", + "cancel": "Cancel", + "resourceConfig": "Configuration Snippets", + "resourceConfigDescription": "Copy and paste these configuration snippets to set up your TCP/UDP resource", + "resourceAddEntrypoints": "Traefik: Add Entrypoints", + "resourceExposePorts": "Gerbil: Expose Ports in Docker Compose", + "resourceLearnRaw": "Learn how to configure TCP/UDP resources", + "resourceBack": "Back to Resources", + "resourceGoTo": "Go to Resource", + "resourceDelete": "Delete Resource", + "resourceDeleteConfirm": "Confirm Delete Resource", + "visibility": "Visibility", + "enabled": "Enabled", + "disabled": "Disabled", + "general": "General", + "generalSettings": "General Settings", + "proxy": "Proxy", + "rules": "Rules", + "resourceSettingDescription": "Configure the settings on your resource", + "resourceSetting": "{resourceName} Settings", + "alwaysAllow": "Always Allow", + "alwaysDeny": "Always Deny", + "orgSettingsDescription": "Configure your organization's general settings", + "orgGeneralSettings": "Organization Settings", + "orgGeneralSettingsDescription": "Manage your organization details and configuration", + "saveGeneralSettings": "Save General Settings", + "saveSettings": "Save Settings", + "orgDangerZone": "Danger Zone", + "orgDangerZoneDescription": "Once you delete this org, there is no going back. Please be certain.", + "orgDelete": "Delete Organization", + "orgDeleteConfirm": "Confirm Delete Organization", + "orgMessageRemove": "This action is irreversible and will delete all associated data.", + "orgMessageConfirm": "To confirm, please type the name of the organization below.", + "orgQuestionRemove": "Are you sure you want to remove the organization {selectedOrg}?", + "orgUpdated": "Organization updated", + "orgUpdatedDescription": "The organization has been updated.", + "orgErrorUpdate": "Failed to update organization", + "orgErrorUpdateMessage": "An error occurred while updating the organization.", + "orgErrorFetch": "Failed to fetch organizations", + "orgErrorFetchMessage": "An error occurred while listing your organizations", + "orgErrorDelete": "Failed to delete organization", + "orgErrorDeleteMessage": "An error occurred while deleting the organization.", + "orgDeleted": "Organization deleted", + "orgDeletedMessage": "The organization and its data has been deleted.", + "orgMissing": "Organization ID Missing", + "orgMissingMessage": "Unable to regenerate invitation without an organization ID.", + "accessUsersManage": "Manage Users", + "accessUsersDescription": "Invite users and add them to roles to manage access to your organization", + "accessUsersSearch": "Search users...", + "accessUserCreate": "Create User", + "accessUserRemove": "Remove User", + "username": "Username", + "identityProvider": "Identity Provider", + "role": "Role", + "nameRequired": "Name is required", + "accessRolesManage": "Manage Roles", + "accessRolesDescription": "Configure roles to manage access to your organization", + "accessRolesSearch": "Search roles...", + "accessRolesAdd": "Add Role", + "accessRoleDelete": "Delete Role", + "description": "Description", + "inviteTitle": "Open Invitations", + "inviteDescription": "Manage your invitations to other users", + "inviteSearch": "Search invitations...", + "minutes": "Minutes", + "hours": "Hours", + "days": "Days", + "weeks": "Weeks", + "months": "Months", + "years": "Years", + "day": "{count, plural, one {# day} other {# days}}", + "apiKeysTitle": "API Key Information", + "apiKeysConfirmCopy2": "You must confirm that you have copied the API key.", + "apiKeysErrorCreate": "Error creating API key", + "apiKeysErrorSetPermission": "Error setting permissions", + "apiKeysCreate": "Generate API Key", + "apiKeysCreateDescription": "Generate a new API key for your organization", + "apiKeysGeneralSettings": "Permissions", + "apiKeysGeneralSettingsDescription": "Determine what this API key can do", + "apiKeysList": "Your API Key", + "apiKeysSave": "Save Your API Key", + "apiKeysSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "apiKeysInfo": "Your API key is:", + "apiKeysConfirmCopy": "I have copied the API key", + "generate": "Generate", + "done": "Done", + "apiKeysSeeAll": "See All API Keys", + "apiKeysPermissionsErrorLoadingActions": "Error loading API key actions", + "apiKeysPermissionsErrorUpdate": "Error setting permissions", + "apiKeysPermissionsUpdated": "Permissions updated", + "apiKeysPermissionsUpdatedDescription": "The permissions have been updated.", + "apiKeysPermissionsGeneralSettings": "Permissions", + "apiKeysPermissionsGeneralSettingsDescription": "Determine what this API key can do", + "apiKeysPermissionsSave": "Save Permissions", + "apiKeysPermissionsTitle": "Permissions", + "apiKeys": "API Keys", + "searchApiKeys": "Search API keys...", + "apiKeysAdd": "Generate API Key", + "apiKeysErrorDelete": "Error deleting API key", + "apiKeysErrorDeleteMessage": "Error deleting API key", + "apiKeysQuestionRemove": "Are you sure you want to remove the API key {selectedApiKey} from the organization?", + "apiKeysMessageRemove": "Once removed, the API key will no longer be able to be used.", + "apiKeysMessageConfirm": "To confirm, please type the name of the API key below.", + "apiKeysDeleteConfirm": "Confirm Delete API Key", + "apiKeysDelete": "Delete API Key", + "apiKeysManage": "Manage API Keys", + "apiKeysDescription": "API keys are used to authenticate with the integration API", + "apiKeysSettings": "{apiKeyName} Settings", + "userTitle": "Manage All Users", + "userDescription": "View and manage all users in the system", + "userAbount": "About User Management", + "userAbountDescription": "This table displays all root user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their root user object - they will remain in the system. To completely remove a user from the system, you must delete their root user object using the delete action in this table.", + "userServer": "Server Users", + "userSearch": "Search server users...", + "userErrorDelete": "Error deleting user", + "userDeleteConfirm": "Confirm Delete User", + "userDeleteServer": "Delete User from Server", + "userMessageRemove": "The user will be removed from all organizations and be completely removed from the server.", + "userMessageConfirm": "To confirm, please type the name of the user below.", + "userQuestionRemove": "Are you sure you want to permanently delete {selectedUser} from the server?", + "licenseKey": "License Key", + "valid": "Valid", + "numberOfSites": "Number of Sites", + "licenseKeySearch": "Search license keys...", + "licenseKeyAdd": "Add License Key", + "type": "Type", + "licenseKeyRequired": "License key is required", + "licenseTermsAgree": "You must agree to the license terms", + "licenseErrorKeyLoad": "Failed to load license keys", + "licenseErrorKeyLoadDescription": "An error occurred loading license keys.", + "licenseErrorKeyDelete": "Failed to delete license key", + "licenseErrorKeyDeleteDescription": "An error occurred deleting license key.", + "licenseKeyDeleted": "License key deleted", + "licenseKeyDeletedDescription": "The license key has been deleted.", + "licenseErrorKeyActivate": "Failed to activate license key", + "licenseErrorKeyActivateDescription": "An error occurred while activating the license key.", + "licenseAbout": "About Licensing", + "communityEdition": "Community Edition", + "licenseAboutDescription": "This is for business and enterprise users who are using Pangolin in a commercial environment. If you are using Pangolin for personal use, you can ignore this section.", + "licenseKeyActivated": "License key activated", + "licenseKeyActivatedDescription": "The license key has been successfully activated.", + "licenseErrorKeyRecheck": "Failed to recheck license keys", + "licenseErrorKeyRecheckDescription": "An error occurred rechecking license keys.", + "licenseErrorKeyRechecked": "License keys rechecked", + "licenseErrorKeyRecheckedDescription": "All license keys have been rechecked", + "licenseActivateKey": "Activate License Key", + "licenseActivateKeyDescription": "Enter a license key to activate it.", + "licenseActivate": "Activate License", + "licenseAgreement": "By checking this box, you confirm that you have read and agree to the license terms corresponding to the tier associated with your license key.", + "fossorialLicense": "View Fossorial Commercial License & Subscription Terms", + "licenseMessageRemove": "This will remove the license key and all associated permissions granted by it.", + "licenseMessageConfirm": "To confirm, please type the license key below.", + "licenseQuestionRemove": "Are you sure you want to delete the license key {selectedKey} ?", + "licenseKeyDelete": "Delete License Key", + "licenseKeyDeleteConfirm": "Confirm Delete License Key", + "licenseTitle": "Manage License Status", + "licenseTitleDescription": "View and manage license keys in the system", + "licenseHost": "Host License", + "licenseHostDescription": "Manage the main license key for the host.", + "licensedNot": "Not Licensed", + "hostId": "Host ID", + "licenseReckeckAll": "Recheck All Keys", + "licenseSiteUsage": "Sites Usage", + "licenseSiteUsageDecsription": "View the number of sites using this license.", + "licenseNoSiteLimit": "There is no limit on the number of sites using an unlicensed host.", + "licensePurchase": "Purchase License", + "licensePurchaseSites": "Purchase Additional Sites", + "licenseSitesUsedMax": "{usedSites} of {maxSites} sites used", + "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} in system.", + "licensePurchaseDescription": "Choose how many sites you want to {selectedMode, select, license {purchase a license for. You can always add more sites later.} other {add to your existing license.}}", + "licenseFee": "License fee", + "licensePriceSite": "Price per site", + "total": "Total", + "licenseContinuePayment": "Continue to Payment", + "pricingPage": "pricing page", + "pricingPortal": "See Purchase Portal", + "licensePricingPage": "For the most up-to-date pricing and discounts, please visit the ", + "invite": "Invitations", + "inviteRegenerate": "Regenerate Invitation", + "inviteRegenerateDescription": "Revoke previous invitation and create a new one", + "inviteRemove": "Remove Invitation", + "inviteRemoveError": "Failed to remove invitation", + "inviteRemoveErrorDescription": "An error occurred while removing the invitation.", + "inviteRemoved": "Invitation removed", + "inviteRemovedDescription": "The invitation for {email} has been removed.", + "inviteQuestionRemove": "Are you sure you want to remove the invitation {email}?", + "inviteMessageRemove": "Once removed, this invitation will no longer be valid. You can always re-invite the user later.", + "inviteMessageConfirm": "To confirm, please type the email address of the invitation below.", + "inviteQuestionRegenerate": "Are you sure you want to regenerate the invitation for {email}? This will revoke the previous invitation.", + "inviteRemoveConfirm": "Confirm Remove Invitation", + "inviteRegenerated": "Invitation Regenerated", + "inviteSent": "A new invitation has been sent to {email}.", + "inviteSentEmail": "Send email notification to the user", + "inviteGenerate": "A new invitation has been generated for {email}.", + "inviteDuplicateError": "Duplicate Invite", + "inviteDuplicateErrorDescription": "An invitation for this user already exists.", + "inviteRateLimitError": "Rate Limit Exceeded", + "inviteRateLimitErrorDescription": "You have exceeded the limit of 3 regenerations per hour. Please try again later.", + "inviteRegenerateError": "Failed to Regenerate Invitation", + "inviteRegenerateErrorDescription": "An error occurred while regenerating the invitation.", + "inviteValidityPeriod": "Validity Period", + "inviteValidityPeriodSelect": "Select validity period", + "inviteRegenerateMessage": "The invitation has been regenerated. The user must access the link below to accept the invitation.", + "inviteRegenerateButton": "Regenerate", + "expiresAt": "Expires At", + "accessRoleUnknown": "Unknown Role", + "placeholder": "Placeholder", + "userErrorOrgRemove": "Failed to remove user", + "userErrorOrgRemoveDescription": "An error occurred while removing the user.", + "userOrgRemoved": "User removed", + "userOrgRemovedDescription": "The user {email} has been removed from the organization.", + "userQuestionOrgRemove": "Are you sure you want to remove {email} from the organization?", + "userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.", + "userMessageOrgConfirm": "To confirm, please type the name of the of the user below.", + "userRemoveOrgConfirm": "Confirm Remove User", + "userRemoveOrg": "Remove User from Organization", + "users": "Users", + "accessRoleMember": "Member", + "accessRoleOwner": "Owner", + "userConfirmed": "Confirmed", + "idpNameInternal": "Internal", + "emailInvalid": "Invalid email address", + "inviteValidityDuration": "Please select a duration", + "accessRoleSelectPlease": "Please select a role", + "usernameRequired": "Username is required", + "idpSelectPlease": "Please select an identity provider", + "idpGenericOidc": "Generic OAuth2/OIDC provider.", + "accessRoleErrorFetch": "Failed to fetch roles", + "accessRoleErrorFetchDescription": "An error occurred while fetching the roles", + "idpErrorFetch": "Failed to fetch identity providers", + "idpErrorFetchDescription": "An error occurred while fetching identity providers", + "userErrorExists": "User Already Exists", + "userErrorExistsDescription": "This user is already a member of the organization.", + "inviteError": "Failed to invite user", + "inviteErrorDescription": "An error occurred while inviting the user", + "userInvited": "User invited", + "userInvitedDescription": "The user has been successfully invited.", + "userErrorCreate": "Failed to create user", + "userErrorCreateDescription": "An error occurred while creating the user", + "userCreated": "User created", + "userCreatedDescription": "The user has been successfully created.", + "userTypeInternal": "Internal User", + "userTypeInternalDescription": "Invite a user to join your organization directly.", + "userTypeExternal": "External User", + "userTypeExternalDescription": "Create a user with an external identity provider.", + "accessUserCreateDescription": "Follow the steps below to create a new user", + "userSeeAll": "See All Users", + "userTypeTitle": "User Type", + "userTypeDescription": "Determine how you want to create the user", + "userSettings": "User Information", + "userSettingsDescription": "Enter the details for the new user", + "inviteEmailSent": "Send invite email to user", + "inviteValid": "Valid For", + "selectDuration": "Select duration", + "accessRoleSelect": "Select role", + "inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.", + "inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.", + "inviteExpiresIn": "The invite will expire in {days, plural, one {# day} other {# days}}.", + "idpTitle": "Identity Provider", + "idpSelect": "Select the identity provider for the external user", + "idpNotConfigured": "No identity providers are configured. Please configure an identity provider before creating external users.", + "usernameUniq": "This must match the unique username that exists in the selected identity provider.", + "emailOptional": "Email (Optional)", + "nameOptional": "Name (Optional)", + "accessControls": "Access Controls", + "userDescription2": "Manage the settings on this user", + "accessRoleErrorAdd": "Failed to add user to role", + "accessRoleErrorAddDescription": "An error occurred while adding user to the role.", + "userSaved": "User saved", + "userSavedDescription": "The user has been updated.", + "accessControlsDescription": "Manage what this user can access and do in the organization", + "accessControlsSubmit": "Save Access Controls", + "roles": "Roles", + "accessUsersRoles": "Manage Users & Roles", + "accessUsersRolesDescription": "Invite users and add them to roles to manage access to your organization", + "key": "Key", + "createdAt": "Created At", + "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", + "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", + "proxyEnableSSL": "Enable SSL (https)", + "targetErrorFetch": "Failed to fetch targets", + "targetErrorFetchDescription": "An error occurred while fetching targets", + "siteErrorFetch": "Failed to fetch resource", + "siteErrorFetchDescription": "An error occurred while fetching resource", + "targetErrorDuplicate": "Duplicate target", + "targetErrorDuplicateDescription": "A target with these settings already exists", + "targetWireGuardErrorInvalidIp": "Invalid target IP", + "targetWireGuardErrorInvalidIpDescription": "Target IP must be within the site subnet", + "targetsUpdated": "Targets updated", + "targetsUpdatedDescription": "Targets and settings updated successfully", + "targetsErrorUpdate": "Failed to update targets", + "targetsErrorUpdateDescription": "An error occurred while updating targets", + "targetTlsUpdate": "TLS settings updated", + "targetTlsUpdateDescription": "Your TLS settings have been updated successfully", + "targetErrorTlsUpdate": "Failed to update TLS settings", + "targetErrorTlsUpdateDescription": "An error occurred while updating TLS settings", + "proxyUpdated": "Proxy settings updated", + "proxyUpdatedDescription": "Your proxy settings have been updated successfully", + "proxyErrorUpdate": "Failed to update proxy settings", + "proxyErrorUpdateDescription": "An error occurred while updating proxy settings", + "targetAddr": "IP / Hostname", + "targetPort": "Port", + "targetProtocol": "Protocol", + "targetTlsSettings": "Secure Connection Configuration", + "targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource", + "targetTlsSettingsAdvanced": "Advanced TLS Settings", + "targetTlsSni": "TLS Server Name (SNI)", + "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", + "targetTlsSubmit": "Save Settings", + "targets": "Targets Configuration", + "targetsDescription": "Set up targets to route traffic to your services", + "targetStickySessions": "Enable Sticky Sessions", + "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", + "methodSelect": "Select method", + "targetSubmit": "Add Target", + "targetNoOne": "No targets. Add a target using the form.", + "targetNoOneDescription": "Adding more than one target above will enable load balancing.", + "targetsSubmit": "Save Targets", + "proxyAdditional": "Additional Proxy Settings", + "proxyAdditionalDescription": "Configure how your resource handles proxy settings", + "proxyCustomHeader": "Custom Host Header", + "proxyCustomHeaderDescription": "The host header to set when proxying requests. Leave empty to use the default.", + "proxyAdditionalSubmit": "Save Proxy Settings", + "subnetMaskErrorInvalid": "Invalid subnet mask. Must be between 0 and 32.", + "ipAddressErrorInvalidFormat": "Invalid IP address format", + "ipAddressErrorInvalidOctet": "Invalid IP address octet", + "path": "Path", + "ipAddressRange": "IP Range", + "rulesErrorFetch": "Failed to fetch rules", + "rulesErrorFetchDescription": "An error occurred while fetching rules", + "rulesErrorDuplicate": "Duplicate rule", + "rulesErrorDuplicateDescription": "A rule with these settings already exists", + "rulesErrorInvalidIpAddressRange": "Invalid CIDR", + "rulesErrorInvalidIpAddressRangeDescription": "Please enter a valid CIDR value", + "rulesErrorInvalidUrl": "Invalid URL path", + "rulesErrorInvalidUrlDescription": "Please enter a valid URL path value", + "rulesErrorInvalidIpAddress": "Invalid IP", + "rulesErrorInvalidIpAddressDescription": "Please enter a valid IP address", + "rulesErrorUpdate": "Failed to update rules", + "rulesErrorUpdateDescription": "An error occurred while updating rules", + "rulesUpdated": "Enable Rules", + "rulesUpdatedDescription": "Rule evaluation has been updated", + "rulesMatchIpAddressRangeDescription": "Enter an address in CIDR format (e.g., 103.21.244.0/22)", + "rulesMatchIpAddress": "Enter an IP address (e.g., 103.21.244.12)", + "rulesMatchUrl": "Enter a URL path or pattern (e.g., /api/v1/todos or /api/v1/*)", + "rulesErrorInvalidPriority": "Invalid Priority", + "rulesErrorInvalidPriorityDescription": "Please enter a valid priority", + "rulesErrorDuplicatePriority": "Duplicate Priorities", + "rulesErrorDuplicatePriorityDescription": "Please enter unique priorities", + "ruleUpdated": "Rules updated", + "ruleUpdatedDescription": "Rules updated successfully", + "ruleErrorUpdate": "Operation failed", + "ruleErrorUpdateDescription": "An error occurred during the save operation", + "rulesPriority": "Priority", + "rulesAction": "Action", + "rulesMatchType": "Match Type", + "value": "Value", + "rulesAbout": "About Rules", + "rulesAboutDescription": "Rules allow you to control access to your resource based on a set of criteria. You can create rules to allow or deny access based on IP address or URL path.", + "rulesActions": "Actions", + "rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods", + "rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted", + "rulesMatchCriteria": "Matching Criteria", + "rulesMatchCriteriaIpAddress": "Match a specific IP address", + "rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation", + "rulesMatchCriteriaUrl": "Match a URL path or pattern", + "rulesEnable": "Enable Rules", + "rulesEnableDescription": "Enable or disable rule evaluation for this resource", + "rulesResource": "Resource Rules Configuration", + "rulesResourceDescription": "Configure rules to control access to your resource", + "ruleSubmit": "Add Rule", + "rulesNoOne": "No rules. Add a rule using the form.", + "rulesOrder": "Rules are evaluated by priority in ascending order.", + "rulesSubmit": "Save Rules", + "resourceErrorCreate": "Error creating resource", + "resourceErrorCreateDescription": "An error occurred when creating the resource", + "resourceErrorCreateMessage": "Error creating resource:", + "resourceErrorCreateMessageDescription": "An unexpected error occurred", + "sitesErrorFetch": "Error fetching sites", + "sitesErrorFetchDescription": "An error occurred when fetching the sites", + "domainsErrorFetch": "Error fetching domains", + "domainsErrorFetchDescription": "An error occurred when fetching the domains", + "none": "None", + "unknown": "Unknown", + "resources": "Resources", + "resourcesDescription": "Resources are proxies to applications running on your private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.", + "resourcesWireGuardConnect": "Secure connectivity with WireGuard encryption", + "resourcesMultipleAuthenticationMethods": "Configure multiple authentication methods", + "resourcesUsersRolesAccess": "User and role-based access control", + "resourcesErrorUpdate": "Failed to toggle resource", + "resourcesErrorUpdateDescription": "An error occurred while updating the resource", + "access": "Access", + "shareLink": "{resource} Share Link", + "resourceSelect": "Select resource", + "shareLinks": "Share Links", + "share": "Shareable Links", + "shareDescription2": "Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one.", + "shareEasyCreate": "Easy to create and share", + "shareConfigurableExpirationDuration": "Configurable expiration duration", + "shareSecureAndRevocable": "Secure and revocable", + "nameMin": "Name must be at least {len} characters.", + "nameMax": "Name must not be longer than {len} characters.", + "sitesConfirmCopy": "Please confirm that you have copied the config.", + "unknownCommand": "Unknown command", + "newtErrorFetchReleases": "Failed to fetch release info: {err}", + "newtErrorFetchLatest": "Error fetching latest release: {err}", + "newtEndpoint": "Newt Endpoint", + "newtId": "Newt ID", + "newtSecretKey": "Newt Secret Key", + "architecture": "Architecture", + "sites": "Sites", + "siteWgAnyClients": "Use any WireGuard client to connect. You will have to address your internal resources using the peer IP.", + "siteWgCompatibleAllClients": "Compatible with all WireGuard clients", + "siteWgManualConfigurationRequired": "Manual configuration required", + "userErrorNotAdminOrOwner": "User is not an admin or owner", + "pangolinSettings": "Settings - Pangolin", + "accessRoleYour": "Your role:", + "accessRoleSelect2": "Select a role", + "accessUserSelect": "Select a user", + "otpEmailEnter": "Enter an email", + "otpEmailEnterDescription": "Press enter to add an email after typing it in the input field.", + "otpEmailErrorInvalid": "Invalid email address. Wildcard (*) must be the entire local part.", + "otpEmailSmtpRequired": "SMTP Required", + "otpEmailSmtpRequiredDescription": "SMTP must be enabled on the server to use one-time password authentication.", + "otpEmailTitle": "One-time Passwords", + "otpEmailTitleDescription": "Require email-based authentication for resource access", + "otpEmailWhitelist": "Email Whitelist", + "otpEmailWhitelistList": "Whitelisted Emails", + "otpEmailWhitelistListDescription": "Only users with these email addresses will be able to access this resource. They will be prompted to enter a one-time password sent to their email. Wildcards (*@example.com) can be used to allow any email address from a domain.", + "otpEmailWhitelistSave": "Save Whitelist", + "passwordAdd": "Add Password", + "passwordRemove": "Remove Password", + "pincodeAdd": "Add PIN Code", + "pincodeRemove": "Remove PIN Code", + "resourceAuthMethods": "Authentication Methods", + "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", + "resourceAuthSettingsSave": "Saved successfully", + "resourceAuthSettingsSaveDescription": "Authentication settings have been saved", + "resourceErrorAuthFetch": "Failed to fetch data", + "resourceErrorAuthFetchDescription": "An error occurred while fetching the data", + "resourceErrorPasswordRemove": "Error removing resource password", + "resourceErrorPasswordRemoveDescription": "An error occurred while removing the resource password", + "resourceErrorPasswordSetup": "Error setting resource password", + "resourceErrorPasswordSetupDescription": "An error occurred while setting the resource password", + "resourceErrorPincodeRemove": "Error removing resource pincode", + "resourceErrorPincodeRemoveDescription": "An error occurred while removing the resource pincode", + "resourceErrorPincodeSetup": "Error setting resource PIN code", + "resourceErrorPincodeSetupDescription": "An error occurred while setting the resource PIN code", + "resourceErrorUsersRolesSave": "Failed to set roles", + "resourceErrorUsersRolesSaveDescription": "An error occurred while setting the roles", + "resourceErrorWhitelistSave": "Failed to save whitelist", + "resourceErrorWhitelistSaveDescription": "An error occurred while saving the whitelist", + "resourcePasswordSubmit": "Enable Password Protection", + "resourcePasswordProtection": "Password Protection {status}", + "resourcePasswordRemove": "Resource password removed", + "resourcePasswordRemoveDescription": "The resource password has been removed successfully", + "resourcePasswordSetup": "Resource password set", + "resourcePasswordSetupDescription": "The resource password has been set successfully", + "resourcePasswordSetupTitle": "Set Password", + "resourcePasswordSetupTitleDescription": "Set a password to protect this resource", + "resourcePincode": "PIN Code", + "resourcePincodeSubmit": "Enable PIN Code Protection", + "resourcePincodeProtection": "PIN Code Protection {status}", + "resourcePincodeRemove": "Resource pincode removed", + "resourcePincodeRemoveDescription": "The resource password has been removed successfully", + "resourcePincodeSetup": "Resource PIN code set", + "resourcePincodeSetupDescription": "The resource pincode has been set successfully", + "resourcePincodeSetupTitle": "Set Pincode", + "resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource", + "resourceRoleDescription": "Admins can always access this resource.", + "resourceUsersRoles": "Users & Roles", + "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", + "resourceUsersRolesSubmit": "Save Users & Roles", + "resourceWhitelistSave": "Saved successfully", + "resourceWhitelistSaveDescription": "Whitelist settings have been saved", + "ssoUse": "Use Platform SSO", + "ssoUseDescription": "Existing users will only have to log in once for all resources that have this enabled.", + "proxyErrorInvalidPort": "Invalid port number", + "subdomainErrorInvalid": "Invalid subdomain", + "domainErrorFetch": "Error fetching domains", + "domainErrorFetchDescription": "An error occurred when fetching the domains", + "resourceErrorUpdate": "Failed to update resource", + "resourceErrorUpdateDescription": "An error occurred while updating the resource", + "resourceUpdated": "Resource updated", + "resourceUpdatedDescription": "The resource has been updated successfully", + "resourceErrorTransfer": "Failed to transfer resource", + "resourceErrorTransferDescription": "An error occurred while transferring the resource", + "resourceTransferred": "Resource transferred", + "resourceTransferredDescription": "The resource has been transferred successfully", + "resourceErrorToggle": "Failed to toggle resource", + "resourceErrorToggleDescription": "An error occurred while updating the resource", + "resourceVisibilityTitle": "Visibility", + "resourceVisibilityTitleDescription": "Completely enable or disable resource visibility", + "resourceGeneral": "General Settings", + "resourceGeneralDescription": "Configure the general settings for this resource", + "resourceEnable": "Enable Resource", + "resourceTransfer": "Transfer Resource", + "resourceTransferDescription": "Transfer this resource to a different site", + "resourceTransferSubmit": "Transfer Resource", + "siteDestination": "Destination Site", + "searchSites": "Search sites", + "accessRoleCreate": "Create Role", + "accessRoleCreateDescription": "Create a new role to group users and manage their permissions.", + "accessRoleCreateSubmit": "Create Role", + "accessRoleCreated": "Role created", + "accessRoleCreatedDescription": "The role has been successfully created.", + "accessRoleErrorCreate": "Failed to create role", + "accessRoleErrorCreateDescription": "An error occurred while creating the role.", + "accessRoleErrorNewRequired": "New role is required", + "accessRoleErrorRemove": "Failed to remove role", + "accessRoleErrorRemoveDescription": "An error occurred while removing the role.", + "accessRoleName": "Role Name", + "accessRoleQuestionRemove": "You're about to delete the {name} role. You cannot undo this action.", + "accessRoleRemove": "Remove Role", + "accessRoleRemoveDescription": "Remove a role from the organization", + "accessRoleRemoveSubmit": "Remove Role", + "accessRoleRemoved": "Role removed", + "accessRoleRemovedDescription": "The role has been successfully removed.", + "accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.", + "manage": "Manage", + "sitesNotFound": "No sites found.", + "pangolinServerAdmin": "Server Admin - Pangolin", + "licenseTierProfessional": "Professional License", + "licenseTierEnterprise": "Enterprise License", + "licenseTierCommercial": "Commercial License", + "licensed": "Licensed", + "yes": "Yes", + "no": "No", + "sitesAdditional": "Additional Sites", + "licenseKeys": "License Keys", + "sitestCountDecrease": "Decrease site count", + "sitestCountIncrease": "Increase site count", + "idpManage": "Manage Identity Providers", + "idpManageDescription": "View and manage identity providers in the system", + "idpDeletedDescription": "Identity provider deleted successfully", + "idpOidc": "OAuth2/OIDC", + "idpQuestionRemove": "Are you sure you want to permanently delete the identity provider {name}?", + "idpMessageRemove": "This will remove the identity provider and all associated configurations. Users who authenticate through this provider will no longer be able to log in.", + "idpMessageConfirm": "To confirm, please type the name of the identity provider below.", + "idpConfirmDelete": "Confirm Delete Identity Provider", + "idpDelete": "Delete Identity Provider", + "idp": "Identity Providers", + "idpSearch": "Search identity providers...", + "idpAdd": "Add Identity Provider", + "idpClientIdRequired": "Client ID is required.", + "idpClientSecretRequired": "Client Secret is required.", + "idpErrorAuthUrlInvalid": "Auth URL must be a valid URL.", + "idpErrorTokenUrlInvalid": "Token URL must be a valid URL.", + "idpPathRequired": "Identifier Path is required.", + "idpScopeRequired": "Scopes are required.", + "idpOidcDescription": "Configure an OpenID Connect identity provider", + "idpCreatedDescription": "Identity provider created successfully", + "idpCreate": "Create Identity Provider", + "idpCreateDescription": "Configure a new identity provider for user authentication", + "idpSeeAll": "See All Identity Providers", + "idpSettingsDescription": "Configure the basic information for your identity provider", + "idpDisplayName": "A display name for this identity provider", + "idpAutoProvisionUsers": "Auto Provision Users", + "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", + "licenseBadge": "Professional", + "idpType": "Provider Type", + "idpTypeDescription": "Select the type of identity provider you want to configure", + "idpOidcConfigure": "OAuth2/OIDC Configuration", + "idpOidcConfigureDescription": "Configure the OAuth2/OIDC provider endpoints and credentials", + "idpClientId": "Client ID", + "idpClientIdDescription": "The OAuth2 client ID from your identity provider", + "idpClientSecret": "Client Secret", + "idpClientSecretDescription": "The OAuth2 client secret from your identity provider", + "idpAuthUrl": "Authorization URL", + "idpAuthUrlDescription": "The OAuth2 authorization endpoint URL", + "idpTokenUrl": "Token URL", + "idpTokenUrlDescription": "The OAuth2 token endpoint URL", + "idpOidcConfigureAlert": "Important Information", + "idpOidcConfigureAlertDescription": "After creating the identity provider, you will need to configure the callback URL in your identity provider's settings. The callback URL will be provided after successful creation.", + "idpToken": "Token Configuration", + "idpTokenDescription": "Configure how to extract user information from the ID token", + "idpJmespathAbout": "About JMESPath", + "idpJmespathAboutDescription": "The paths below use JMESPath syntax to extract values from the ID token.", + "idpJmespathAboutDescriptionLink": "Learn more about JMESPath", + "idpJmespathLabel": "Identifier Path", + "idpJmespathLabelDescription": "The path to the user identifier in the ID token", + "idpJmespathEmailPathOptional": "Email Path (Optional)", + "idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token", + "idpJmespathNamePathOptional": "Name Path (Optional)", + "idpJmespathNamePathOptionalDescription": "The path to the user's name in the ID token", + "idpOidcConfigureScopes": "Scopes", + "idpOidcConfigureScopesDescription": "Space-separated list of OAuth2 scopes to request", + "idpSubmit": "Create Identity Provider", + "orgPolicies": "Organization Policies", + "idpSettings": "{idpName} Settings", + "idpCreateSettingsDescription": "Configure the settings for your identity provider", + "roleMapping": "Role Mapping", + "orgMapping": "Organization Mapping", + "orgPoliciesSearch": "Search organization policies...", + "orgPoliciesAdd": "Add Organization Policy", + "orgRequired": "Organization is required", + "error": "Error", + "success": "Success", + "orgPolicyAddedDescription": "Policy added successfully", + "orgPolicyUpdatedDescription": "Policy updated successfully", + "orgPolicyDeletedDescription": "Policy deleted successfully", + "defaultMappingsUpdatedDescription": "Default mappings updated successfully", + "orgPoliciesAbout": "About Organization Policies", + "orgPoliciesAboutDescription": "Organization policies are used to control access to organizations based on the user's ID token. You can specify JMESPath expressions to extract role and organization information from the ID token.", + "orgPoliciesAboutDescriptionLink": "See documentation, for more information.", + "defaultMappingsOptional": "Default Mappings (Optional)", + "defaultMappingsOptionalDescription": "The default mappings are used when when there is not an organization policy defined for an organization. You can specify the default role and organization mappings to fall back to here.", + "defaultMappingsRole": "Default Role Mapping", + "defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.", + "defaultMappingsOrg": "Default Organization Mapping", + "defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.", + "defaultMappingsSubmit": "Save Default Mappings", + "orgPoliciesEdit": "Edit Organization Policy", + "org": "Organization", + "orgSelect": "Select organization", + "orgSearch": "Search org", + "orgNotFound": "No org found.", + "roleMappingPathOptional": "Role Mapping Path (Optional)", + "orgMappingPathOptional": "Organization Mapping Path (Optional)", + "orgPolicyUpdate": "Update Policy", + "orgPolicyAdd": "Add Policy", + "orgPolicyConfig": "Configure access for an organization", + "idpUpdatedDescription": "Identity provider updated successfully", + "redirectUrl": "Redirect URL", + "redirectUrlAbout": "About Redirect URL", + "redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in your identity provider settings.", + "pangolinAuth": "Auth - Pangolin", + "verificationCodeLengthRequirements": "Your verification code must be 8 characters.", + "errorOccurred": "An error occurred", + "emailErrorVerify": "Failed to verify email:", + "emailVerified": "Email successfully verified! Redirecting you...", + "verificationCodeErrorResend": "Failed to resend verification code:", + "verificationCodeResend": "Verification code resent", + "verificationCodeResendDescription": "We've resent a verification code to your email address. Please check your inbox.", + "emailVerify": "Verify Email", + "emailVerifyDescription": "Enter the verification code sent to your email address.", + "verificationCode": "Verification Code", + "verificationCodeEmailSent": "We sent a verification code to your email address.", + "submit": "Submit", + "emailVerifyResendProgress": "Resending...", + "emailVerifyResend": "Didn't receive a code? Click here to resend", + "passwordNotMatch": "Passwords do not match", + "signupError": "An error occurred while signing up", + "pangolinLogoAlt": "Pangolin Logo", + "inviteAlready": "Looks like you've been invited!", + "inviteAlreadyDescription": "To accept the invite, you must log in or create an account.", + "signupQuestion": "Already have an account?", + "login": "Log in", + "resourceNotFound": "Resource Not Found", + "resourceNotFoundDescription": "The resource you're trying to access does not exist.", + "pincodeRequirementsLength": "PIN must be exactly 6 digits", + "pincodeRequirementsChars": "PIN must only contain numbers", + "passwordRequirementsLength": "Password must be at least 1 character long", + "otpEmailRequirementsLength": "OTP must be at least 1 character long", + "otpEmailSent": "OTP Sent", + "otpEmailSentDescription": "An OTP has been sent to your email", + "otpEmailErrorAuthenticate": "Failed to authenticate with email", + "pincodeErrorAuthenticate": "Failed to authenticate with pincode", + "passwordErrorAuthenticate": "Failed to authenticate with password", + "poweredBy": "Powered by", + "authenticationRequired": "Authentication Required", + "authenticationMethodChoose": "Choose your preferred method to access {name}", + "authenticationRequest": "You must authenticate to access {name}", + "user": "User", + "pincodeInput": "6-digit PIN Code", + "pincodeSubmit": "Log in with PIN", + "passwordSubmit": "Log In with Password", + "otpEmailDescription": "A one-time code will be sent to this email.", + "otpEmailSend": "Send One-time Code", + "otpEmail": "One-Time Password (OTP)", + "otpEmailSubmit": "Submit OTP", + "backToEmail": "Back to Email", + "noSupportKey": "Server is running without a supporter key. Consider supporting the project!", + "accessDenied": "Access Denied", + "accessDeniedDescription": "You're not allowed to access this resource. If this is a mistake, please contact the administrator.", + "accessTokenError": "Error checking access token", + "accessGranted": "Access Granted", + "accessUrlInvalid": "Access URL Invalid", + "accessGrantedDescription": "You have been granted access to this resource. Redirecting you...", + "accessUrlInvalidDescription": "This shared access URL is invalid. Please contact the resource owner for a new URL.", + "tokenInvalid": "Invalid token", + "pincodeInvalid": "Invalid code", + "passwordErrorRequestReset": "Failed to request reset:", + "passwordErrorReset": "Failed to reset password:", + "passwordResetSuccess": "Password reset successfully! Back to log in...", + "passwordReset": "Reset Password", + "passwordResetDescription": "Follow the steps to reset your password", + "passwordResetSent": "We'll send a password reset code to this email address.", + "passwordResetCode": "Reset Code", + "passwordResetCodeDescription": "Check your email for the reset code.", + "passwordNew": "New Password", + "passwordNewConfirm": "Confirm New Password", + "pincodeAuth": "Authenticator Code", + "pincodeSubmit2": "Submit Code", + "passwordResetSubmit": "Request Reset", + "passwordBack": "Back to Password", + "loginBack": "Go back to log in", + "signup": "Sign up", + "loginStart": "Log in to get started", + "idpOidcTokenValidating": "Validating OIDC token", + "idpOidcTokenResponse": "Validate OIDC token response", + "idpErrorOidcTokenValidating": "Error validating OIDC token", + "idpConnectingTo": "Connecting to {name}", + "idpConnectingToDescription": "Validating your identity", + "idpConnectingToProcess": "Connecting...", + "idpConnectingToFinished": "Connected", + "idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.", + "idpErrorNotFound": "IdP not found", + "inviteInvalid": "Invalid Invite", + "inviteInvalidDescription": "The invite link is invalid.", + "inviteErrorWrongUser": "Invite is not for this user", + "inviteErrorUserNotExists": "User does not exist. Please create an account first.", + "inviteErrorLoginRequired": "You must be logged in to accept an invite", + "inviteErrorExpired": "The invite may have expired", + "inviteErrorRevoked": "The invite might have been revoked", + "inviteErrorTypo": "There could be a typo in the invite link", + "pangolinSetup": "Setup - Pangolin", + "orgNameRequired": "Organization name is required", + "orgIdRequired": "Organization ID is required", + "orgErrorCreate": "An error occurred while creating org", + "pageNotFound": "Page Not Found", + "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", + "overview": "Overview", + "home": "Home", + "accessControl": "Access Control", + "settings": "Settings", + "usersAll": "All Users", + "license": "License", + "pangolinDashboard": "Dashboard - Pangolin", + "noResults": "No results found.", + "terabytes": "{count} TB", + "gigabytes": "{count} GB", + "megabytes": "{count} MB", + "tagsEntered": "Entered Tags", + "tagsEnteredDescription": "These are the tags you`ve entered.", + "tagsWarnCannotBeLessThanZero": "maxTags and minTags cannot be less than 0", + "tagsWarnNotAllowedAutocompleteOptions": "Tag not allowed as per autocomplete options", + "tagsWarnInvalid": "Invalid tag as per validateTag", + "tagWarnTooShort": "Tag {tagText} is too short", + "tagWarnTooLong": "Tag {tagText} is too long", + "tagsWarnReachedMaxNumber": "Reached the maximum number of tags allowed", + "tagWarnDuplicate": "Duplicate tag {tagText} not added", + "supportKeyInvalid": "Invalid Key", + "supportKeyInvalidDescription": "Your supporter key is invalid.", + "supportKeyValid": "Valid Key", + "supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!", + "supportKeyErrorValidationDescription": "Failed to validate supporter key.", + "supportKey": "Support Development and Adopt a Pangolin!", + "supportKeyDescription": "Purchase a supporter key to help us continue developing Pangolin for the community. Your contribution allows us to commit more time to maintain and add new features to the application for everyone. We will never use this to paywall features. This is separate from any Commercial Edition.", + "supportKeyPet": "You will also get to adopt and meet your very own pet Pangolin!", + "supportKeyPurchase": "Payments are processed via GitHub. Afterward, you can retrieve your key on", + "supportKeyPurchaseLink": "our website", + "supportKeyPurchase2": "and redeem it here.", + "supportKeyLearnMore": "Learn more.", + "supportKeyOptions": "Please select the option that best suits you.", + "supportKetOptionFull": "Full Supporter", + "forWholeServer": "For the whole server", + "lifetimePurchase": "Lifetime purchase", + "supporterStatus": "Supporter status", + "buy": "Buy", + "supportKeyOptionLimited": "Limited Supporter", + "forFiveUsers": "For 5 or less users", + "supportKeyRedeem": "Redeem Supporter Key", + "supportKeyHideSevenDays": "Hide for 7 days", + "supportKeyEnter": "Enter Supporter Key", + "supportKeyEnterDescription": "Meet your very own pet Pangolin!", + "githubUsername": "GitHub Username", + "supportKeyInput": "Supporter Key", + "supportKeyBuy": "Buy Supporter Key", + "logoutError": "Error logging out", + "signingAs": "Signed in as", + "serverAdmin": "Server Admin", + "otpEnable": "Enable Two-factor", + "otpDisable": "Disable Two-factor", + "logout": "Log Out", + "licenseTierProfessionalRequired": "Professional Edition Required", + "licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.", + "actionGetOrg": "Get Organization", + "actionUpdateOrg": "Update Organization", + "actionUpdateUser": "Update User", + "actionGetUser": "Get User", + "actionGetOrgUser": "Get Organization User", + "actionListOrgDomains": "List Organization Domains", + "actionCreateSite": "Create Site", + "actionDeleteSite": "Delete Site", + "actionGetSite": "Get Site", + "actionListSites": "List Sites", + "actionUpdateSite": "Update Site", + "actionListSiteRoles": "List Allowed Site Roles", + "actionCreateResource": "Create Resource", + "actionDeleteResource": "Delete Resource", + "actionGetResource": "Get Resource", + "actionListResource": "List Resources", + "actionUpdateResource": "Update Resource", + "actionListResourceUsers": "List Resource Users", + "actionSetResourceUsers": "Set Resource Users", + "actionSetAllowedResourceRoles": "Set Allowed Resource Roles", + "actionListAllowedResourceRoles": "List Allowed Resource Roles", + "actionSetResourcePassword": "Set Resource Password", + "actionSetResourcePincode": "Set Resource Pincode", + "actionSetResourceEmailWhitelist": "Set Resource Email Whitelist", + "actionGetResourceEmailWhitelist": "Get Resource Email Whitelist", + "actionCreateTarget": "Create Target", + "actionDeleteTarget": "Delete Target", + "actionGetTarget": "Get Target", + "actionListTargets": "List Targets", + "actionUpdateTarget": "Update Target", + "actionCreateRole": "Create Role", + "actionDeleteRole": "Delete Role", + "actionGetRole": "Get Role", + "actionListRole": "List Roles", + "actionUpdateRole": "Update Role", + "actionListAllowedRoleResources": "List Allowed Role Resources", + "actionInviteUser": "Invite User", + "actionRemoveUser": "Remove User", + "actionListUsers": "List Users", + "actionAddUserRole": "Add User Role", + "actionGenerateAccessToken": "Generate Access Token", + "actionDeleteAccessToken": "Delete Access Token", + "actionListAccessTokens": "List Access Tokens", + "actionCreateResourceRule": "Create Resource Rule", + "actionDeleteResourceRule": "Delete Resource Rule", + "actionListResourceRules": "List Resource Rules", + "actionUpdateResourceRule": "Update Resource Rule", + "actionListOrgs": "List Organizations", + "actionCheckOrgId": "Check ID", + "actionCreateOrg": "Create Organization", + "actionDeleteOrg": "Delete Organization", + "actionListApiKeys": "List API Keys", + "actionListApiKeyActions": "List API Key Actions", + "actionSetApiKeyActions": "Set API Key Allowed Actions", + "actionCreateApiKey": "Create API Key", + "actionDeleteApiKey": "Delete API Key", + "actionCreateIdp": "Create IDP", + "actionUpdateIdp": "Update IDP", + "actionDeleteIdp": "Delete IDP", + "actionListIdps": "List IDP", + "actionGetIdp": "Get IDP", + "actionCreateIdpOrg": "Create IDP Org Policy", + "actionDeleteIdpOrg": "Delete IDP Org Policy", + "actionListIdpOrgs": "List IDP Orgs", + "actionUpdateIdpOrg": "Update IDP Org", + "actionCreateClient": "Create Client", + "actionDeleteClient": "Delete Client", + "actionUpdateClient": "Update Client", + "actionListClients": "List Clients", + "actionGetClient": "Get Client", + "noneSelected": "None selected", + "orgNotFound2": "No organizations found.", + "searchProgress": "Search...", + "create": "Create", + "orgs": "Organizations", + "loginError": "An error occurred while logging in", + "passwordForgot": "Forgot your password?", + "otpAuth": "Two-Factor Authentication", + "otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.", + "otpAuthSubmit": "Submit Code", + "idpContinue": "Or continue with", + "otpAuthBack": "Back to Log In", + "navbar": "Navigation Menu", + "navbarDescription": "Main navigation menu for the application", + "navbarDocsLink": "Documentation", + "commercialEdition": "Commercial Edition", + "otpErrorEnable": "Unable to enable 2FA", + "otpErrorEnableDescription": "An error occurred while enabling 2FA", + "otpSetupCheckCode": "Please enter a 6-digit code", + "otpSetupCheckCodeRetry": "Invalid code. Please try again.", + "otpSetup": "Enable Two-factor Authentication", + "otpSetupDescription": "Secure your account with an extra layer of protection", + "otpSetupScanQr": "Scan this QR code with your authenticator app or enter the secret key manually:", + "otpSetupSecretCode": "Authenticator Code", + "otpSetupSuccess": "Two-Factor Authentication Enabled", + "otpSetupSuccessStoreBackupCodes": "Your account is now more secure. Don't forget to save your backup codes.", + "otpErrorDisable": "Unable to disable 2FA", + "otpErrorDisableDescription": "An error occurred while disabling 2FA", + "otpRemove": "Disable Two-factor Authentication", + "otpRemoveDescription": "Disable two-factor authentication for your account", + "otpRemoveSuccess": "Two-Factor Authentication Disabled", + "otpRemoveSuccessMessage": "Two-factor authentication has been disabled for your account. You can enable it again at any time.", + "otpRemoveSubmit": "Disable 2FA", + "paginator": "Page {current} of {last}", + "paginatorToFirst": "Go to first page", + "paginatorToPrevious": "Go to previous page", + "paginatorToNext": "Go to next page", + "paginatorToLast": "Go to last page", + "copyText": "Copy text", + "copyTextFailed": "Failed to copy text: ", + "copyTextClipboard": "Copy to clipboard", + "inviteErrorInvalidConfirmation": "Invalid confirmation", + "passwordRequired": "Password is required", + "allowAll": "Allow All", + "permissionsAllowAll": "Allow All Permissions", + "githubUsernameRequired": "GitHub username is required", + "supportKeyRequired": "Supporter key is required", + "passwordRequirementsChars": "Password must be at least 8 characters", + "language": "Language", + "verificationCodeRequired": "Code is required", + "userErrorNoUpdate": "No user to update", + "siteErrorNoUpdate": "No site to update", + "resourceErrorNoUpdate": "No resource to update", + "authErrorNoUpdate": "No auth info to update", + "orgErrorNoUpdate": "No org to update", + "orgErrorNoProvided": "No org provided", + "apiKeysErrorNoUpdate": "No API key to update", + "sidebarOverview": "Overview", + "sidebarHome": "Home", + "sidebarSites": "Sites", + "sidebarResources": "Resources", + "sidebarAccessControl": "Access Control", + "sidebarUsers": "Users", + "sidebarInvitations": "Invitations", + "sidebarRoles": "Roles", + "sidebarShareableLinks": "Shareable Links", + "sidebarApiKeys": "API Keys", + "sidebarSettings": "Settings", + "sidebarAllUsers": "All Users", + "sidebarIdentityProviders": "Identity Providers", + "sidebarLicense": "License", + "sidebarClients": "Clients (Beta)", + "sidebarDomains": "Domains", + "enableDockerSocket": "Enable Docker Socket", + "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", + "enableDockerSocketLink": "Learn More", + "viewDockerContainers": "View Docker Containers", + "containersIn": "Containers in {siteName}", + "selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.", + "containerName": "Name", + "containerImage": "Image", + "containerState": "State", + "containerNetworks": "Networks", + "containerHostnameIp": "Hostname/IP", + "containerLabels": "Labels", + "containerLabelsCount": "{count, plural, one {# label} other {# labels}}", + "containerLabelsTitle": "Container Labels", + "containerLabelEmpty": "", + "containerPorts": "Ports", + "containerPortsMore": "+{count} more", + "containerActions": "Actions", + "select": "Select", + "noContainersMatchingFilters": "No containers found matching the current filters.", + "showContainersWithoutPorts": "Show containers without ports", + "showStoppedContainers": "Show stopped containers", + "noContainersFound": "No containers found. Make sure Docker containers are running.", + "searchContainersPlaceholder": "Search across {count} containers...", + "searchResultsCount": "{count, plural, one {# result} other {# results}}", + "filters": "Filters", + "filterOptions": "Filter Options", + "filterPorts": "Ports", + "filterStopped": "Stopped", + "clearAllFilters": "Clear all filters", + "columns": "Columns", + "toggleColumns": "Toggle Columns", + "refreshContainersList": "Refresh containers list", + "searching": "Searching...", + "noContainersFoundMatching": "No containers found matching \"{filter}\".", + "light": "light", + "dark": "dark", + "system": "system", + "theme": "Theme", + "subnetRequired": "Subnet is required", + "initialSetupTitle": "Initial Server Setup", + "initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.", + "createAdminAccount": "Create Admin Account", + "setupErrorCreateAdmin": "An error occurred while creating the server admin account.", + "certificateStatus": "Certificate Status", + "loading": "Loading", + "restart": "Restart", + "domains": "Domains", + "domainsDescription": "Manage domains for your organization", + "domainsSearch": "Search domains...", + "domainAdd": "Add Domain", + "domainAddDescription": "Register a new domain with your organization", + "domainCreate": "Create Domain", + "domainCreatedDescription": "Domain created successfully", + "domainDeletedDescription": "Domain deleted successfully", + "domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?", + "domainMessageRemove": "Once removed, the domain will no longer be associated with your account.", + "domainMessageConfirm": "To confirm, please type the domain name below.", + "domainConfirmDelete": "Confirm Delete Domain", + "domainDelete": "Delete Domain", + "domain": "Domain", + "selectDomainTypeNsName": "Domain Delegation (NS)", + "selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.", + "selectDomainTypeCnameName": "Single Domain (CNAME)", + "selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.", + "selectDomainTypeWildcardName": "Wildcard Domain", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "domainDelegation": "Single Domain", + "selectType": "Select a type", + "actions": "Actions", + "refresh": "Refresh", + "refreshError": "Failed to refresh data", + "verified": "Verified", + "pending": "Pending", + "sidebarBilling": "Billing", + "billing": "Billing", + "orgBillingDescription": "Manage your billing information and subscriptions", + "github": "GitHub", + "pangolinHosted": "Pangolin Hosted", + "fossorial": "Fossorial", + "completeAccountSetup": "Complete Account Setup", + "completeAccountSetupDescription": "Set your password to get started", + "accountSetupSent": "We'll send an account setup code to this email address.", + "accountSetupCode": "Setup Code", + "accountSetupCodeDescription": "Check your email for the setup code.", + "passwordCreate": "Create Password", + "passwordCreateConfirm": "Confirm Password", + "accountSetupSubmit": "Send Setup Code", + "completeSetup": "Complete Setup", + "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", + "documentation": "Documentation", + "saveAllSettings": "Save All Settings", + "settingsUpdated": "Settings updated", + "settingsUpdatedDescription": "All settings have been updated successfully", + "settingsErrorUpdate": "Failed to update settings", + "settingsErrorUpdateDescription": "An error occurred while updating settings", + "sidebarCollapse": "Collapse", + "sidebarExpand": "Expand", + "newtUpdateAvailable": "Update Available", + "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", + "domainPickerEnterDomain": "Domain", + "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", + "domainPickerDescription": "Enter the full domain of the resource to see available options.", + "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", + "domainPickerTabAll": "All", + "domainPickerTabOrganization": "Organization", + "domainPickerTabProvided": "Provided", + "domainPickerSortAsc": "A-Z", + "domainPickerSortDesc": "Z-A", + "domainPickerCheckingAvailability": "Checking availability...", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerOrganizationDomains": "Organization Domains", + "domainPickerProvidedDomains": "Provided Domains", + "domainPickerSubdomain": "Subdomain: {subdomain}", + "domainPickerNamespace": "Namespace: {namespace}", + "domainPickerShowMore": "Show More", + "domainNotFound": "Domain Not Found", + "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", + "failed": "Failed", + "createNewOrgDescription": "Create a new organization", + "organization": "Organization", + "port": "Port", + "securityKeyManage": "Manage Security Keys", + "securityKeyDescription": "Add or remove security keys for passwordless authentication", + "securityKeyRegister": "Register New Security Key", + "securityKeyList": "Your Security Keys", + "securityKeyNone": "No security keys registered yet", + "securityKeyNameRequired": "Name is required", + "securityKeyRemove": "Remove", + "securityKeyLastUsed": "Last used: {date}", + "securityKeyNameLabel": "Security Key Name", + "securityKeyRegisterSuccess": "Security key registered successfully", + "securityKeyRegisterError": "Failed to register security key", + "securityKeyRemoveSuccess": "Security key removed successfully", + "securityKeyRemoveError": "Failed to remove security key", + "securityKeyLoadError": "Failed to load security keys", + "securityKeyLogin": "Continue with security key", + "securityKeyAuthError": "Failed to authenticate with security key", + "securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.", + "registering": "Registering...", + "securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.", + "securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.", + "securityKeyPermissionDenied": "Please allow access to your security key to continue signing in.", + "securityKeyRemovedTooQuickly": "Please keep your security key connected until the sign-in process completes.", + "securityKeyNotSupported": "Your security key may not be compatible. Please try a different security key.", + "securityKeyUnknownError": "There was a problem using your security key. Please try again.", + "twoFactorRequired": "Two-factor authentication is required to register a security key.", + "twoFactor": "Two-Factor Authentication", + "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", + "continueToApplication": "Continue to Application", + "securityKeyAdd": "Add Security Key", + "securityKeyRegisterTitle": "Register New Security Key", + "securityKeyRegisterDescription": "Connect your security key and enter a name to identify it", + "securityKeyTwoFactorRequired": "Two-Factor Authentication Required", + "securityKeyTwoFactorDescription": "Please enter your two-factor authentication code to register the security key", + "securityKeyTwoFactorRemoveDescription": "Please enter your two-factor authentication code to remove the security key", + "securityKeyTwoFactorCode": "Two-Factor Code", + "securityKeyRemoveTitle": "Remove Security Key", + "securityKeyRemoveDescription": "Enter your password to remove the security key \"{name}\"", + "securityKeyNoKeysRegistered": "No security keys registered", + "securityKeyNoKeysDescription": "Add a security key to enhance your account security", + "createDomainRequired": "Domain is required", + "createDomainAddDnsRecords": "Add DNS Records", + "createDomainAddDnsRecordsDescription": "Add the following DNS records to your domain provider to complete the setup.", + "createDomainNsRecords": "NS Records", + "createDomainRecord": "Record", + "createDomainType": "Type:", + "createDomainName": "Name:", + "createDomainValue": "Value:", + "createDomainCnameRecords": "CNAME Records", + "createDomainARecords": "A Records", + "createDomainRecordNumber": "Record {number}", + "createDomainTxtRecords": "TXT Records", + "createDomainSaveTheseRecords": "Save These Records", + "createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.", + "createDomainDnsPropagation": "DNS Propagation", + "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", + "resourcePortRequired": "Port number is required for non-HTTP resources", + "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" +} From 664dbf3f4c00a45a9487ee9d4e2cac8af0675c66 Mon Sep 17 00:00:00 2001 From: Sebastian Felber Date: Mon, 4 Aug 2025 15:45:33 +0000 Subject: [PATCH 037/219] make IPv6 optional during install --- install/config/docker-compose.yml | 2 +- install/input.txt | 1 + install/main.go | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 4ce31e41..560ab530 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -60,4 +60,4 @@ networks: default: driver: bridge name: pangolin - enable_ipv6: true +{{if .EnableIPv6}} enable_ipv6: true{{end}} diff --git a/install/input.txt b/install/input.txt index 9ecf0d4d..12df39d7 100644 --- a/install/input.txt +++ b/install/input.txt @@ -1,6 +1,7 @@ docker example.com pangolin.example.com +yes admin@example.com yes admin@example.com diff --git a/install/main.go b/install/main.go index 9bb0c7e1..cea107d2 100644 --- a/install/main.go +++ b/install/main.go @@ -39,6 +39,7 @@ type Config struct { BadgerVersion string BaseDomain string DashboardDomain string + EnableIPv6 bool LetsEncryptEmail string EnableEmail bool EmailSMTPHost string @@ -303,6 +304,7 @@ func collectUserInput(reader *bufio.Reader) Config { fmt.Println("\n=== Basic Configuration ===") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain) + config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) From 1a9de1e5c5c85f95e4770a4b6f15281decf8847e Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 Aug 2025 20:17:35 -0700 Subject: [PATCH 038/219] Move endpoint to per site --- server/db/pg/schema.ts | 5 +- server/db/sqlite/schema.ts | 5 +- server/routers/client/updateClient.ts | 37 ++- server/routers/gerbil/getAllRelays.ts | 16 +- server/routers/gerbil/updateHolePunch.ts | 231 ++++++++++++------ server/routers/newt/handleGetConfigMessage.ts | 5 +- .../routers/olm/handleOlmRegisterMessage.ts | 17 +- 7 files changed, 208 insertions(+), 108 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index be4e58e2..d307f399 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -516,7 +516,7 @@ export const clients = pgTable("clients", { lastPing: varchar("lastPing"), type: varchar("type").notNull(), // "olm" online: boolean("online").notNull().default(false), - endpoint: varchar("endpoint"), + // endpoint: varchar("endpoint"), lastHolePunch: integer("lastHolePunch"), maxConnections: integer("maxConnections") }); @@ -528,7 +528,8 @@ export const clientSites = pgTable("clientSites", { siteId: integer("siteId") .notNull() .references(() => sites.siteId, { onDelete: "cascade" }), - isRelayed: boolean("isRelayed").notNull().default(false) + isRelayed: boolean("isRelayed").notNull().default(false), + endpoint: varchar("endpoint") }); export const olms = pgTable("olms", { diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 5773a5f3..10f6686e 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -216,7 +216,7 @@ export const clients = sqliteTable("clients", { lastPing: text("lastPing"), type: text("type").notNull(), // "olm" online: integer("online", { mode: "boolean" }).notNull().default(false), - endpoint: text("endpoint"), + // endpoint: text("endpoint"), lastHolePunch: integer("lastHolePunch") }); @@ -227,7 +227,8 @@ export const clientSites = sqliteTable("clientSites", { siteId: integer("siteId") .notNull() .references(() => sites.siteId, { onDelete: "cascade" }), - isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false) + isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false), + endpoint: text("endpoint") }); export const olms = sqliteTable("olms", { diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 60a48732..de4a7b5e 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -129,7 +129,7 @@ export async function updateClient( `Adding ${sitesAdded.length} new sites to client ${client.clientId}` ); for (const siteId of sitesAdded) { - if (!client.subnet || !client.pubKey || !client.endpoint) { + if (!client.subnet || !client.pubKey) { logger.debug( "Client subnet, pubKey or endpoint is not set" ); @@ -140,10 +140,25 @@ export async function updateClient( // BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS const isRelayed = true; + // get the clientsite + const [clientSite] = await db + .select() + .from(clientSites) + .where(and( + eq(clientSites.clientId, client.clientId), + eq(clientSites.siteId, siteId) + )) + .limit(1); + + if (!clientSite || !clientSite.endpoint) { + logger.debug("Client site is missing or has no endpoint"); + continue; + } + const site = await newtAddPeer(siteId, { publicKey: client.pubKey, allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: isRelayed ? "" : client.endpoint + endpoint: isRelayed ? "" : clientSite.endpoint }); if (!site) { @@ -255,7 +270,6 @@ export async function updateClient( } } - if (client.endpoint) { // get all sites for this client and join with exit nodes with site.exitNodeId const sitesData = await db .select() @@ -272,6 +286,8 @@ export async function updateClient( let exitNodeDestinations: { reachableAt: string; + sourceIp: string; + sourcePort: number; destinations: PeerDestination[]; }[] = []; @@ -282,6 +298,14 @@ export async function updateClient( ); continue; } + + if (!site.clientSites.endpoint) { + logger.warn( + `Site ${site.sites.siteId} has no endpoint, skipping` + ); + continue; + } + // find the destinations in the array let destinations = exitNodeDestinations.find( (d) => d.reachableAt === site.exitNodes?.reachableAt @@ -290,6 +314,8 @@ export async function updateClient( if (!destinations) { destinations = { reachableAt: site.exitNodes?.reachableAt || "", + sourceIp: site.clientSites.endpoint.split(":")[0] || "", + sourcePort: parseInt(site.clientSites.endpoint.split(":")[1]) || 0, destinations: [ { destinationIP: @@ -319,8 +345,8 @@ export async function updateClient( `Updating destinations for exit node at ${destination.reachableAt}` ); const payload = { - sourceIp: client.endpoint?.split(":")[0] || "", - sourcePort: parseInt(client.endpoint?.split(":")[1]) || 0, + sourceIp: destination.sourceIp, + sourcePort: destination.sourcePort, destinations: destination.destinations }; logger.info( @@ -351,7 +377,6 @@ export async function updateClient( } } } - } // Fetch the updated client const [updatedClient] = await trx diff --git a/server/routers/gerbil/getAllRelays.ts b/server/routers/gerbil/getAllRelays.ts index abe4d593..8d1c66b2 100644 --- a/server/routers/gerbil/getAllRelays.ts +++ b/server/routers/gerbil/getAllRelays.ts @@ -78,19 +78,13 @@ export async function getAllRelays( .where(eq(clientSites.siteId, site.siteId)); for (const clientSite of clientSitesRes) { - // Get client information - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientSite.clientId)); - - if (!client || !client.endpoint) { + if (!clientSite.endpoint) { continue; } // Add this site as a destination for the client - if (!mappings[client.endpoint]) { - mappings[client.endpoint] = { destinations: [] }; + if (!mappings[clientSite.endpoint]) { + mappings[clientSite.endpoint] = { destinations: [] }; } // Add site as a destination for this client @@ -100,13 +94,13 @@ export async function getAllRelays( }; // Check if this destination is already in the array to avoid duplicates - const isDuplicate = mappings[client.endpoint].destinations.some( + const isDuplicate = mappings[clientSite.endpoint].destinations.some( dest => dest.destinationIP === destination.destinationIP && dest.destinationPort === destination.destinationPort ); if (!isDuplicate) { - mappings[client.endpoint].destinations.push(destination); + mappings[clientSite.endpoint].destinations.push(destination); } } diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 836061d6..39771454 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -1,8 +1,16 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { clients, newts, olms, Site, sites, clientSites, exitNodes } from "@server/db"; +import { + clients, + newts, + olms, + Site, + sites, + clientSites, + exitNodes +} from "@server/db"; import { db } from "@server/db"; -import { eq } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; @@ -19,7 +27,8 @@ const updateHolePunchSchema = z.object({ ip: z.string(), port: z.number(), timestamp: z.number(), - reachableAt: z.string().optional() + reachableAt: z.string().optional(), + publicKey: z.string() }); // New response type with multi-peer destination support @@ -45,13 +54,24 @@ export async function updateHolePunch( ); } - const { olmId, newtId, ip, port, timestamp, token, reachableAt } = parsedParams.data; + const { + olmId, + newtId, + ip, + port, + timestamp, + token, + reachableAt, + publicKey + } = parsedParams.data; let currentSiteId: number | undefined; let destinations: PeerDestination[] = []; - + if (olmId) { - logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`); + logger.debug( + `Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}${publicKey ? ` with exit node publicKey: ${publicKey}` : ""}` + ); const { session, olm: olmSession } = await validateOlmSessionToken(token); @@ -62,7 +82,9 @@ export async function updateHolePunch( } if (olmId !== olmSession.olmId) { - logger.warn(`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`); + logger.warn( + `Olm ID mismatch: ${olmId} !== ${olmSession.olmId}` + ); return next( createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized") ); @@ -83,12 +105,55 @@ export async function updateHolePunch( const [client] = await db .update(clients) .set({ - endpoint: `${ip}:${port}`, lastHolePunch: timestamp }) .where(eq(clients.clientId, olm.clientId)) .returning(); - + + // Get the exit node by public key + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.publicKey, publicKey)); + + if (exitNode) { + // Get sites that are on this specific exit node and connected to this client + const sitesOnExitNode = await db + .select({ siteId: sites.siteId }) + .from(sites) + .innerJoin( + clientSites, + eq(sites.siteId, clientSites.siteId) + ) + .where( + and( + eq(sites.exitNodeId, exitNode.exitNodeId), + eq(clientSites.clientId, olm.clientId) + ) + ); + + // Update clientSites for each site on this exit node + for (const site of sitesOnExitNode) { + await db + .update(clientSites) + .set({ + endpoint: `${ip}:${port}` + }) + .where( + and( + eq(clientSites.clientId, olm.clientId), + eq(clientSites.siteId, site.siteId) + ) + ); + } + + logger.debug( + `Updated ${sitesOnExitNode.length} sites on exit node with publicKey: ${publicKey}` + ); + } else { + logger.warn(`Exit node not found for publicKey: ${publicKey}`); + } + if (!client) { logger.warn(`Client not found for olm: ${olmId}`); return next( @@ -101,23 +166,23 @@ export async function updateHolePunch( // .select() // .from(clientSites) // .where(eq(clientSites.clientId, client.clientId)); - + // if (clientSitePairs.length === 0) { // logger.warn(`No sites found for client: ${client.clientId}`); // return next( // createHttpError(HttpCode.NOT_FOUND, "No sites found for client") // ); // } - + // // Get all sites details // const siteIds = clientSitePairs.map(pair => pair.siteId); - + // for (const siteId of siteIds) { // const [site] = await db // .select() // .from(sites) // .where(eq(sites.siteId, siteId)); - + // if (site && site.subnet && site.listenPort) { // destinations.push({ // destinationIP: site.subnet.split("/")[0], @@ -141,7 +206,9 @@ export async function updateHolePunch( for (const site of sitesData) { if (!site.sites.subnet) { - logger.warn(`Site ${site.sites.siteId} has no subnet, skipping`); + logger.warn( + `Site ${site.sites.siteId} has no subnet, skipping` + ); continue; } // find the destinations in the array @@ -176,51 +243,55 @@ export async function updateHolePunch( logger.debug(JSON.stringify(exitNodeDestinations, null, 2)); - for (const destination of exitNodeDestinations) { - // if its the current exit node skip it because it is replying with the same data - if (reachableAt && destination.reachableAt == reachableAt) { - logger.debug(`Skipping update for reachableAt: ${reachableAt}`); - continue; - } + // BECAUSE OF HARD NAT YOU DONT WANT TO SEND THE OLM IP AND PORT TO THE ALL THE OTHER EXIT NODES + // BECAUSE THEY WILL GET A DIFFERENT IP AND PORT - try { - const response = await axios.post( - `${destination.reachableAt}/update-destinations`, - { - sourceIp: client.endpoint?.split(":")[0] || "", - sourcePort: parseInt(client.endpoint?.split(":")[1] || "0"), - destinations: destination.destinations - }, - { - headers: { - "Content-Type": "application/json" - } - } - ); + // for (const destination of exitNodeDestinations) { + // // if its the current exit node skip it because it is replying with the same data + // if (reachableAt && destination.reachableAt == reachableAt) { + // logger.debug(`Skipping update for reachableAt: ${reachableAt}`); + // continue; + // } - logger.info("Destinations updated:", { - peer: response.data.status - }); - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error( - `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}` - ); - } else { - logger.error( - `Error updating destinations for exit node at ${destination.reachableAt}: ${error}` - ); - } - } - } + // try { + // const response = await axios.post( + // `${destination.reachableAt}/update-destinations`, + // { + // sourceIp: client.endpoint?.split(":")[0] || "", + // sourcePort: parseInt(client.endpoint?.split(":")[1] || "0"), + // destinations: destination.destinations + // }, + // { + // headers: { + // "Content-Type": "application/json" + // } + // } + // ); + + // logger.info("Destinations updated:", { + // peer: response.data.status + // }); + // } catch (error) { + // if (axios.isAxiosError(error)) { + // logger.error( + // `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}` + // ); + // } else { + // logger.error( + // `Error updating destinations for exit node at ${destination.reachableAt}: ${error}` + // ); + // } + // } + // } // Send the desinations back to the origin - destinations = exitNodeDestinations.find( - (d) => d.reachableAt === reachableAt - )?.destinations || []; - + destinations = + exitNodeDestinations.find((d) => d.reachableAt === reachableAt) + ?.destinations || []; } else if (newtId) { - logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`); + logger.debug( + `Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}` + ); const { session, newt: newtSession } = await validateNewtSessionToken(token); @@ -232,7 +303,9 @@ export async function updateHolePunch( } if (newtId !== newtSession.newtId) { - logger.warn(`Newt ID mismatch: ${newtId} !== ${newtSession.newtId}`); + logger.warn( + `Newt ID mismatch: ${newtId} !== ${newtSession.newtId}` + ); return next( createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized") ); @@ -261,7 +334,7 @@ export async function updateHolePunch( }) .where(eq(sites.siteId, newt.siteId)) .returning(); - + if (!updatedSite || !updatedSite.subnet) { logger.warn(`Site not found: ${newt.siteId}`); return next( @@ -274,7 +347,7 @@ export async function updateHolePunch( // .select() // .from(clientSites) // .where(eq(clientSites.siteId, newt.siteId)); - + // THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING // Get client details for each client // for (const pair of sitesClientPairs) { @@ -282,7 +355,7 @@ export async function updateHolePunch( // .select() // .from(clients) // .where(eq(clients.clientId, pair.clientId)); - + // if (client && client.endpoint) { // const [host, portStr] = client.endpoint.split(':'); // if (host && portStr) { @@ -293,27 +366,27 @@ export async function updateHolePunch( // } // } // } - + // If this is a newt/site, also add other sites in the same org - // if (updatedSite.orgId) { - // const orgSites = await db - // .select() - // .from(sites) - // .where(eq(sites.orgId, updatedSite.orgId)); - - // for (const site of orgSites) { - // // Don't add the current site to the destinations - // if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) { - // const [host, portStr] = site.endpoint.split(':'); - // if (host && portStr) { - // destinations.push({ - // destinationIP: host, - // destinationPort: site.listenPort - // }); - // } - // } - // } - // } + // if (updatedSite.orgId) { + // const orgSites = await db + // .select() + // .from(sites) + // .where(eq(sites.orgId, updatedSite.orgId)); + + // for (const site of orgSites) { + // // Don't add the current site to the destinations + // if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) { + // const [host, portStr] = site.endpoint.split(':'); + // if (host && portStr) { + // destinations.push({ + // destinationIP: host, + // destinationPort: site.listenPort + // }); + // } + // } + // } + // } } // if (destinations.length === 0) { @@ -336,4 +409,4 @@ export async function updateHolePunch( ) ); } -} \ No newline at end of file +} diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 1059847c..9ab1c049 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -157,9 +157,6 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { if (!client.clients.subnet) { return false; } - if (!client.clients.endpoint) { - return false; - } return true; }) .map(async (client) => { @@ -215,7 +212,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { allowedIps: [`${client.clients.subnet.split("/")[0]}/32`], // we want to only allow from that client endpoint: client.clientSites.isRelayed ? "" - : client.clients.endpoint! // if its relayed it should be localhost + : client.clientSites.endpoint! // if its relayed it should be localhost }; }) ); diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 32e4fe51..7028a2f0 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -8,7 +8,7 @@ import { olms, sites } from "@server/db"; -import { eq, inArray } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; @@ -147,15 +147,24 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { continue; } + const [clientSite] = await db + .select() + .from(clientSites) + .where(and( + eq(clientSites.clientId, client.clientId), + eq(clientSites.siteId, site.siteId) + )) + .limit(1); + // Add the peer to the exit node for this site - if (client.endpoint) { + if (clientSite.endpoint) { logger.info( - `Adding peer ${publicKey} to site ${site.siteId} with endpoint ${client.endpoint}` + `Adding peer ${publicKey} to site ${site.siteId} with endpoint ${clientSite.endpoint}` ); await addPeer(site.siteId, { publicKey: publicKey, allowedIps: [`${client.subnet.split('/')[0]}/32`], // we want to only allow from that client - endpoint: relay ? "" : client.endpoint + endpoint: relay ? "" : clientSite.endpoint }); } else { logger.warn( From 5889efd74a5af74ca0ef569ff8fbc9e1cfc035be Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 Aug 2025 20:22:13 -0700 Subject: [PATCH 039/219] Send all hp data now --- .../routers/olm/handleOlmRegisterMessage.ts | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 7028a2f0..88390668 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,13 +1,6 @@ import { db, ExitNode } from "@server/db"; import { MessageHandler } from "../ws"; -import { - clients, - clientSites, - exitNodes, - Olm, - olms, - sites -} from "@server/db"; +import { clients, clientSites, exitNodes, Olm, olms, sites } from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; @@ -30,7 +23,9 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { const clientId = olm.clientId; const { publicKey, relay } = message.data; - logger.debug(`Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}`); + logger.debug( + `Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}` + ); if (!publicKey) { logger.warn("Public key not provided"); @@ -50,22 +45,25 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } if (client.exitNodeId) { - // Get the exit node for this site - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, client.exitNodeId)) - .limit(1); + // TODO: FOR NOW WE ARE JUST HOLEPUNCHING ALL EXIT NODES BUT IN THE FUTURE WE SHOULD HANDLE THIS BETTER - // Send holepunch message for each site - sendToClient(olm.olmId, { + // Get the exit node + const allExitNodes = await db.select().from(exitNodes); + + const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => { + return { + serverPubKey: exitNode.publicKey, + endpoint: exitNode.endpoint + }; + }); + + // Send holepunch message + await sendToClient(olm.olmId, { type: "olm/wg/holepunch", data: { - serverPubKey: exitNode.publicKey, - endpoint: exitNode.endpoint, + exitNodes: exitNodesHpData, } }); - } if (now - (client.lastHolePunch || 0) > 6) { @@ -103,7 +101,9 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // Prepare an array to store site configurations let siteConfigurations = []; - logger.debug(`Found ${sitesData.length} sites for client ${client.clientId}`); + logger.debug( + `Found ${sitesData.length} sites for client ${client.clientId}` + ); if (sitesData.length === 0) { sendToClient(olm.olmId, { @@ -150,10 +150,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { const [clientSite] = await db .select() .from(clientSites) - .where(and( - eq(clientSites.clientId, client.clientId), - eq(clientSites.siteId, site.siteId) - )) + .where( + and( + eq(clientSites.clientId, client.clientId), + eq(clientSites.siteId, site.siteId) + ) + ) .limit(1); // Add the peer to the exit node for this site @@ -163,7 +165,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { ); await addPeer(site.siteId, { publicKey: publicKey, - allowedIps: [`${client.subnet.split('/')[0]}/32`], // we want to only allow from that client + allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client endpoint: relay ? "" : clientSite.endpoint }); } else { @@ -197,7 +199,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { }); } - // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES + // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES // if (siteConfigurations.length === 0) { // logger.warn("No valid site configurations found"); // return; From 6b1808dab1d630ea40471db22184cd1637f6c633 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 Aug 2025 20:34:27 -0700 Subject: [PATCH 040/219] Handle multiple hp messages --- server/routers/olm/handleOlmRegisterMessage.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 88390668..64443e07 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -52,16 +52,25 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => { return { - serverPubKey: exitNode.publicKey, + publicKey: exitNode.publicKey, endpoint: exitNode.endpoint }; }); // Send holepunch message await sendToClient(olm.olmId, { - type: "olm/wg/holepunch", + type: "olm/wg/holepunch/all", data: { - exitNodes: exitNodesHpData, + exitNodes: exitNodesHpData + } + }); + + // THIS IS FOR BACKWARDS COMPATIBILITY + await sendToClient(olm.olmId, { + type: "olm/wg/holepunch/all", + data: { + serverPubKey: allExitNodes[0].publicKey, + endpoint: allExitNodes[0].endpoint } }); } From dcf530d23743ff283a1ee0871b1a1c8bc329dfe0 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 Aug 2025 20:36:25 -0700 Subject: [PATCH 041/219] Add backward compatability --- server/routers/gerbil/updateHolePunch.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 39771454..c01d707e 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -7,7 +7,8 @@ import { Site, sites, clientSites, - exitNodes + exitNodes, + ExitNode } from "@server/db"; import { db } from "@server/db"; import { eq, and } from "drizzle-orm"; @@ -110,11 +111,20 @@ export async function updateHolePunch( .where(eq(clients.clientId, olm.clientId)) .returning(); + let exitNode: ExitNode | undefined; + if (publicKey) { // Get the exit node by public key - const [exitNode] = await db + [exitNode] = await db .select() .from(exitNodes) .where(eq(exitNodes.publicKey, publicKey)); + } else { + // FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0 + [exitNode] = await db + .select() + .from(exitNodes) + .limit(1) + } if (exitNode) { // Get sites that are on this specific exit node and connected to this client From 17cf90380472019a6bb97b4fc4addb3c8330f700 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 Aug 2025 21:29:40 -0700 Subject: [PATCH 042/219] publicKey optional --- server/routers/gerbil/updateHolePunch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index c01d707e..fe4503b8 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -29,7 +29,7 @@ const updateHolePunchSchema = z.object({ port: z.number(), timestamp: z.number(), reachableAt: z.string().optional(), - publicKey: z.string() + publicKey: z.string().optional() }); // New response type with multi-peer destination support From f6440753b6d9fe561f1e813af64dcaa4c2c0c298 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 Aug 2025 21:34:07 -0700 Subject: [PATCH 043/219] Only update proxy mapping if there is an existing --- server/routers/newt/handleGetConfigMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 9ab1c049..b2594a71 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -102,7 +102,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { .from(exitNodes) .where(eq(exitNodes.exitNodeId, site.exitNodeId)) .limit(1); - if (exitNode.reachableAt) { + if (exitNode.reachableAt && existingSite.subnet && existingSite.listenPort) { try { const response = await axios.post( `${exitNode.reachableAt}/update-proxy-mapping`, From b2947193ec6e60ee1599d0a970d0b6c60c9de911 Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:35:22 +0800 Subject: [PATCH 044/219] Integrate setup token into installer, this will now parse the container logs to extract setup token automatically. Displays token with clear instructions and URL for initial admin setup. --- install/main.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 3 deletions(-) diff --git a/install/main.go b/install/main.go index a6d9d686..ccf82e70 100644 --- a/install/main.go +++ b/install/main.go @@ -202,6 +202,28 @@ func main() { } } else { fmt.Println("Looks like you already installed, so I am going to do the setup...") + + // Read existing config to get DashboardDomain + traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") + if err != nil { + fmt.Printf("Warning: Could not read existing config: %v\n", err) + fmt.Println("You may need to manually enter your domain information.") + config = collectUserInput(reader) + } else { + config.DashboardDomain = traefikConfig.DashboardDomain + config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail + config.BadgerVersion = traefikConfig.BadgerVersion + + // Show detected values and allow user to confirm or re-enter + fmt.Println("Detected existing configuration:") + fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain) + fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail) + fmt.Printf("Badger Version: %s\n", config.BadgerVersion) + + if !readBool(reader, "Are these values correct?", true) { + config = collectUserInput(reader) + } + } } if !checkIsCrowdsecInstalledInCompose() { @@ -239,6 +261,23 @@ func main() { } } + // Setup Token Section + fmt.Println("\n=== Setup Token ===") + + // Check if containers were started during this installation + containersStarted := false + if (isDockerInstalled() && chosenContainer == Docker) || + (isPodmanInstalled() && chosenContainer == Podman) { + // Try to fetch and display the token if containers are running + containersStarted = true + printSetupToken(chosenContainer, config.DashboardDomain) + } + + // If containers weren't started or token wasn't found, show instructions + if !containersStarted { + showSetupTokenInstructions(chosenContainer, config.DashboardDomain) + } + fmt.Println("Installation complete!") fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } @@ -302,7 +341,13 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain) + + // Set default dashboard domain after base domain is collected + defaultDashboardDomain := "" + if config.BaseDomain != "" { + defaultDashboardDomain = "pangolin." + config.BaseDomain + } + config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) @@ -625,8 +670,8 @@ func pullContainers(containerType SupportedContainer) error { } if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { - return fmt.Errorf("failed to pull the containers: %v", err) + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) } return nil @@ -755,6 +800,91 @@ func waitForContainer(containerName string, containerType SupportedContainer) er return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) } +func printSetupToken(containerType SupportedContainer, dashboardDomain string) { + fmt.Println("Waiting for Pangolin to generate setup token...") + + // Wait for Pangolin to be healthy + if err := waitForContainer("pangolin", containerType); err != nil { + fmt.Println("Warning: Pangolin container did not become healthy in time.") + return + } + + // Give a moment for the setup token to be generated + time.Sleep(2 * time.Second) + + // Fetch logs + var cmd *exec.Cmd + if containerType == Docker { + cmd = exec.Command("docker", "logs", "pangolin") + } else { + cmd = exec.Command("podman", "logs", "pangolin") + } + output, err := cmd.Output() + if err != nil { + fmt.Println("Warning: Could not fetch Pangolin logs to find setup token.") + return + } + + // Parse for setup token + lines := strings.Split(string(output), "\n") + for i, line := range lines { + if strings.Contains(line, "=== SETUP TOKEN GENERATED ===") || strings.Contains(line, "=== SETUP TOKEN EXISTS ===") { + // Look for "Token: ..." in the next few lines + for j := i + 1; j < i+5 && j < len(lines); j++ { + trimmedLine := strings.TrimSpace(lines[j]) + if strings.Contains(trimmedLine, "Token:") { + // Extract token after "Token:" + tokenStart := strings.Index(trimmedLine, "Token:") + if tokenStart != -1 { + token := strings.TrimSpace(trimmedLine[tokenStart+6:]) + fmt.Printf("Setup token: %s\n", token) + fmt.Println("") + fmt.Println("This token is required to register the first admin account in the web UI at:") + fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain) + fmt.Println("") + fmt.Println("Save this token securely. It will be invalid after the first admin is created.") + return + } + } + } + } + } + fmt.Println("Warning: Could not find a setup token in Pangolin logs.") +} + +func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomain string) { + fmt.Println("\n=== Setup Token Instructions ===") + fmt.Println("To get your setup token, you need to:") + fmt.Println("") + fmt.Println("1. Start the containers:") + if containerType == Docker { + fmt.Println(" docker-compose up -d") + } else { + fmt.Println(" podman-compose up -d") + } + fmt.Println("") + fmt.Println("2. Wait for the Pangolin container to start and generate the token") + fmt.Println("") + fmt.Println("3. Check the container logs for the setup token:") + if containerType == Docker { + fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") + } else { + fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") + } + fmt.Println("") + fmt.Println("4. Look for output like:") + fmt.Println(" === SETUP TOKEN GENERATED ===") + fmt.Println(" Token: [your-token-here]") + fmt.Println(" Use this token on the initial setup page") + fmt.Println("") + fmt.Println("5. Use the token to complete initial setup at:") + fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain) + fmt.Println("") + fmt.Println("The setup token is required to register the first admin account.") + fmt.Println("Save it securely - it will be invalid after the first admin is created.") + fmt.Println("================================") +} + func generateRandomSecretKey() string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const length = 32 From fe5c91db29ccd5f5927451c69622df85b0746f9a Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 5 Aug 2025 11:25:54 -0700 Subject: [PATCH 045/219] Change how you send the desitnations --- server/routers/gerbil/receiveBandwidth.ts | 2 +- server/routers/gerbil/updateHolePunch.ts | 213 +++++----------------- 2 files changed, 51 insertions(+), 164 deletions(-) diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index 5e672d0f..caadf7bb 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -31,7 +31,7 @@ export const receiveBandwidth = async ( const currentTime = new Date(); const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago - logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`); + // logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`); await db.transaction(async (trx) => { // First, handle sites that are actively reporting bandwidth diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index fe4503b8..14b1921c 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -111,59 +111,59 @@ export async function updateHolePunch( .where(eq(clients.clientId, olm.clientId)) .returning(); - let exitNode: ExitNode | undefined; + let exitNode: ExitNode | undefined; if (publicKey) { - // Get the exit node by public key - [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.publicKey, publicKey)); - } else { - // FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0 + // Get the exit node by public key [exitNode] = await db .select() .from(exitNodes) - .limit(1) + .where(eq(exitNodes.publicKey, publicKey)); + } else { + // FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0 + [exitNode] = await db.select().from(exitNodes).limit(1); } - if (exitNode) { - // Get sites that are on this specific exit node and connected to this client - const sitesOnExitNode = await db - .select({ siteId: sites.siteId }) - .from(sites) - .innerJoin( - clientSites, - eq(sites.siteId, clientSites.siteId) + if (!exitNode) { + logger.warn(`Exit node not found for publicKey: ${publicKey}`); + return next( + createHttpError(HttpCode.NOT_FOUND, "Exit node not found") + ); + } + + // Get sites that are on this specific exit node and connected to this client + const sitesOnExitNode = await db + .select({ siteId: sites.siteId, subnet: sites.subnet, listenPort: sites.listenPort }) + .from(sites) + .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) + .where( + and( + eq(sites.exitNodeId, exitNode.exitNodeId), + eq(clientSites.clientId, olm.clientId) ) + ); + + // Update clientSites for each site on this exit node + for (const site of sitesOnExitNode) { + logger.debug( + `Updating site ${site.siteId} on exit node with publicKey: ${publicKey}` + ); + + await db + .update(clientSites) + .set({ + endpoint: `${ip}:${port}` + }) .where( and( - eq(sites.exitNodeId, exitNode.exitNodeId), - eq(clientSites.clientId, olm.clientId) + eq(clientSites.clientId, olm.clientId), + eq(clientSites.siteId, site.siteId) ) ); - - // Update clientSites for each site on this exit node - for (const site of sitesOnExitNode) { - await db - .update(clientSites) - .set({ - endpoint: `${ip}:${port}` - }) - .where( - and( - eq(clientSites.clientId, olm.clientId), - eq(clientSites.siteId, site.siteId) - ) - ); - } - - logger.debug( - `Updated ${sitesOnExitNode.length} sites on exit node with publicKey: ${publicKey}` - ); - } else { - logger.warn(`Exit node not found for publicKey: ${publicKey}`); } + logger.debug( + `Updated ${sitesOnExitNode.length} sites on exit node with publicKey: ${publicKey}` + ); if (!client) { logger.warn(`Client not found for olm: ${olmId}`); return next( @@ -171,133 +171,16 @@ export async function updateHolePunch( ); } - // // Get all sites that this client is connected to - // const clientSitePairs = await db - // .select() - // .from(clientSites) - // .where(eq(clientSites.clientId, client.clientId)); - - // if (clientSitePairs.length === 0) { - // logger.warn(`No sites found for client: ${client.clientId}`); - // return next( - // createHttpError(HttpCode.NOT_FOUND, "No sites found for client") - // ); - // } - - // // Get all sites details - // const siteIds = clientSitePairs.map(pair => pair.siteId); - - // for (const siteId of siteIds) { - // const [site] = await db - // .select() - // .from(sites) - // .where(eq(sites.siteId, siteId)); - - // if (site && site.subnet && site.listenPort) { - // destinations.push({ - // destinationIP: site.subnet.split("/")[0], - // destinationPort: site.listenPort - // }); - // } - // } - - // get all sites for this client and join with exit nodes with site.exitNodeId - const sitesData = await db - .select() - .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) - .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) - .where(eq(clientSites.clientId, client.clientId)); - - let exitNodeDestinations: { - reachableAt: string; - destinations: PeerDestination[]; - }[] = []; - - for (const site of sitesData) { - if (!site.sites.subnet) { - logger.warn( - `Site ${site.sites.siteId} has no subnet, skipping` - ); - continue; - } - // find the destinations in the array - let destinations = exitNodeDestinations.find( - (d) => d.reachableAt === site.exitNodes?.reachableAt - ); - - if (!destinations) { - destinations = { - reachableAt: site.exitNodes?.reachableAt || "", - destinations: [ - { - destinationIP: site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 - } - ] - }; - } else { - // add to the existing destinations - destinations.destinations.push({ - destinationIP: site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 + // Create a list of the destinations from the sites + for (const site of sitesOnExitNode) { + if (site.subnet && site.listenPort) { + destinations.push({ + destinationIP: ip, + destinationPort: site.listenPort }); } - - // update it in the array - exitNodeDestinations = exitNodeDestinations.filter( - (d) => d.reachableAt !== site.exitNodes?.reachableAt - ); - exitNodeDestinations.push(destinations); } - logger.debug(JSON.stringify(exitNodeDestinations, null, 2)); - - // BECAUSE OF HARD NAT YOU DONT WANT TO SEND THE OLM IP AND PORT TO THE ALL THE OTHER EXIT NODES - // BECAUSE THEY WILL GET A DIFFERENT IP AND PORT - - // for (const destination of exitNodeDestinations) { - // // if its the current exit node skip it because it is replying with the same data - // if (reachableAt && destination.reachableAt == reachableAt) { - // logger.debug(`Skipping update for reachableAt: ${reachableAt}`); - // continue; - // } - - // try { - // const response = await axios.post( - // `${destination.reachableAt}/update-destinations`, - // { - // sourceIp: client.endpoint?.split(":")[0] || "", - // sourcePort: parseInt(client.endpoint?.split(":")[1] || "0"), - // destinations: destination.destinations - // }, - // { - // headers: { - // "Content-Type": "application/json" - // } - // } - // ); - - // logger.info("Destinations updated:", { - // peer: response.data.status - // }); - // } catch (error) { - // if (axios.isAxiosError(error)) { - // logger.error( - // `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}` - // ); - // } else { - // logger.error( - // `Error updating destinations for exit node at ${destination.reachableAt}: ${error}` - // ); - // } - // } - // } - - // Send the desinations back to the origin - destinations = - exitNodeDestinations.find((d) => d.reachableAt === reachableAt) - ?.destinations || []; } else if (newtId) { logger.debug( `Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}` @@ -406,6 +289,10 @@ export async function updateHolePunch( // return next(createHttpError(HttpCode.NOT_FOUND, "No peer destinations found")); // } + logger.debug( + `Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}` + ); + // Return the new multi-peer structure return res.status(HttpCode.OK).send({ destinations: destinations From d557832509a5e3a19efa5cd67f7c072ea3917dca Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 5 Aug 2025 11:37:45 -0700 Subject: [PATCH 046/219] Send this right IP this time --- server/routers/gerbil/updateHolePunch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 14b1921c..1d30b1ea 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -175,7 +175,7 @@ export async function updateHolePunch( for (const site of sitesOnExitNode) { if (site.subnet && site.listenPort) { destinations.push({ - destinationIP: ip, + destinationIP: site.subnet.split("/")[0], destinationPort: site.listenPort }); } From e6589308ddd0e1054089ce64796505fa016ae667 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 7 Aug 2025 14:53:46 -0700 Subject: [PATCH 047/219] Update readme --- README.md | 2 +- messages/en-US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c09fdb24..ee888428 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. ## Contributions -Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). +Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue. Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. diff --git a/messages/en-US.json b/messages/en-US.json index 9986c5fd..d1234d72 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" From b2384ccc06e1cc362a0028f53654d2892e35e24f Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:12 -0700 Subject: [PATCH 048/219] New translations en-us.json (French) --- messages/fr-FR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 4d23e073..bb1a4ac3 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version d'Olm.", "remoteSubnets": "Sous-réseaux distants", "enterCidrRange": "Entrez la plage CIDR", - "remoteSubnetsDescription": "Ajoutez des plages CIDR pouvant accéder à ce site à distance. Utilisez le format comme 10.0.0.0/24 ou 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Activer le proxy public", "resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.", "externalProxyEnabled": "Proxy externe activé" From 4755cae5cbd6d2fad515d75d50c53433215fd1a6 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:14 -0700 Subject: [PATCH 049/219] New translations en-us.json (Spanish) --- messages/es-ES.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index 5bd43502..7fabb18c 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.", "remoteSubnets": "Subredes remotas", "enterCidrRange": "Ingresa el rango CIDR", - "remoteSubnetsDescription": "Agregue rangos CIDR que puedan acceder a este sitio de forma remota. Use un formato como 10.0.0.0/24 o 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Habilitar proxy público", "resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.", "externalProxyEnabled": "Proxy externo habilitado" From 6b6ff0a95e3ec5cf105074880ac7622e29d15e22 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:15 -0700 Subject: [PATCH 050/219] New translations en-us.json (Czech) --- messages/cs-CZ.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index b2152580..6fe79036 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" From bd4be2b05cbfc6f69d6448c3dcdf62cd5595d01b Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:16 -0700 Subject: [PATCH 051/219] New translations en-us.json (German) --- messages/de-DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index 50aac219..e82fb44a 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.", "remoteSubnets": "Remote-Subnetze", "enterCidrRange": "Geben Sie den CIDR-Bereich ein", - "remoteSubnetsDescription": "Fügen Sie CIDR-Bereiche hinzu, die aus der Ferne auf diesen Standort zugreifen können. Verwenden Sie das Format wie 10.0.0.0/24 oder 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Öffentlichen Proxy aktivieren", "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", "externalProxyEnabled": "Externer Proxy aktiviert" From 6e08a70afc96dc1ac3d6c76e7e5fbaa13b6e6c3a Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:17 -0700 Subject: [PATCH 052/219] New translations en-us.json (Italian) --- messages/it-IT.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index d336011a..651259eb 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.", "remoteSubnets": "Sottoreti Remote", "enterCidrRange": "Inserisci l'intervallo CIDR", - "remoteSubnetsDescription": "Aggiungi intervalli CIDR che possono accedere a questo sito da remoto. Usa il formato come 10.0.0.0/24 o 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Abilita Proxy Pubblico", "resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.", "externalProxyEnabled": "Proxy Esterno Abilitato" From fa8f49e87d86c67021f8f73c227eefc7d033b6cc Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:18 -0700 Subject: [PATCH 053/219] New translations en-us.json (Korean) --- messages/ko-KR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index c70d34ff..0c28db0f 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" From 56da7c242d94464404fde3cbf4f133a236699fd2 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:20 -0700 Subject: [PATCH 054/219] New translations en-us.json (Dutch) --- messages/nl-NL.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 38f68a3b..68ccfeae 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.", "remoteSubnets": "Externe Subnets", "enterCidrRange": "Voer CIDR-bereik in", - "remoteSubnetsDescription": "Voeg CIDR-bereiken toe die deze site op afstand kunnen openen. Gebruik een format zoals 10.0.0.0/24 of 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Openbare proxy inschakelen", "resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.", "externalProxyEnabled": "Externe Proxy Ingeschakeld" From b3502bd6278a7b18da10122f07f81b79ad743191 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:21 -0700 Subject: [PATCH 055/219] New translations en-us.json (Polish) --- messages/pl-PL.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 0f1eb29a..0df783a5 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.", "remoteSubnets": "Zdalne Podsieci", "enterCidrRange": "Wprowadź zakres CIDR", - "remoteSubnetsDescription": "Dodaj zakresy CIDR, które mogą uzyskać zdalny dostęp do tej witryny. Użyj formatu takiego jak 10.0.0.0/24 lub 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Włącz publiczny proxy", "resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.", "externalProxyEnabled": "Zewnętrzny Proxy Włączony" From 8bc353442f7e21709130da4648ca73a5eaad3e8d Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:22 -0700 Subject: [PATCH 056/219] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 9a3104bd..c126ba1c 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.", "remoteSubnets": "Sub-redes Remotas", "enterCidrRange": "Insira o intervalo CIDR", - "remoteSubnetsDescription": "Adicione intervalos CIDR que podem acessar este site remotamente. Use o formato como 10.0.0.0/24 ou 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Ativar Proxy Público", "resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.", "externalProxyEnabled": "Proxy Externo Habilitado" From a88be89c2f0018cf2335c3975d8982d5c8b34065 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:24 -0700 Subject: [PATCH 057/219] New translations en-us.json (Turkish) --- messages/tr-TR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index e4d68eba..8b9e2450 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.", "remoteSubnets": "Uzak Alt Ağlar", "enterCidrRange": "CIDR aralığını girin", - "remoteSubnetsDescription": "Bu siteye uzaktan erişebilecek CIDR aralıklarını ekleyin. 10.0.0.0/24 veya 192.168.1.0/24 gibi formatlar kullanın.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Genel Proxy'i Etkinleştir", "resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.", "externalProxyEnabled": "Dış Proxy Etkinleştirildi" From 387dbc360eac7123e9ba36a7b18a5362a831cddf Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:29 -0700 Subject: [PATCH 058/219] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index b18a7ab7..6172738c 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。", "remoteSubnets": "远程子网", "enterCidrRange": "输入 CIDR 范围", - "remoteSubnetsDescription": "添加能远程访问此站点的 CIDR 范围。使用格式如 10.0.0.0/24 或 192.168.1.0/24。", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "启用公共代理", "resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。", "externalProxyEnabled": "外部代理已启用" From 95c0f6c0930ad70667511d304ecee26de1cc14d5 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:30 -0700 Subject: [PATCH 059/219] New translations en-us.json (Russian) --- messages/ru-RU.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 90d3804d..62360ecc 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" From 0ce430cab5978c836d0501cac3f8357263bcfae3 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 7 Aug 2025 15:04:32 -0700 Subject: [PATCH 060/219] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index bf786a24..738fe3ed 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -1320,7 +1320,7 @@ "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" From b4284f82f3d14a1f76235783eba3b93a8cd86ed9 Mon Sep 17 00:00:00 2001 From: EliasT05 <46528691+EliasTors@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:23:09 +0200 Subject: [PATCH 061/219] Added nb-NO translation --- messages/nb-NO.json | 1323 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1323 insertions(+) create mode 100644 messages/nb-NO.json diff --git a/messages/nb-NO.json b/messages/nb-NO.json new file mode 100644 index 00000000..320418e0 --- /dev/null +++ b/messages/nb-NO.json @@ -0,0 +1,1323 @@ +{ + "setupCreate": "Lag din organisasjon, område og dine ressurser", + "setupNewOrg": "Ny Organisasjon", + "setupCreateOrg": "Opprett organisasjon", + "setupCreateResources": "Opprett ressurser", + "setupOrgName": "Organisasjonsnavn", + "orgDisplayName": "Dette er visningsnavnet til organisasjonen din.", + "orgId": "Organisasjons-ID", + "setupIdentifierMessage": "Dette er den unike identifikator for din organisasjon. Dette er separat fra visningsnavnet.", + "setupErrorIdentifier": "Organisasjons-ID er allerede tatt. Vennligst velg en annen.", + "componentsErrorNoMemberCreate": "Du er for øyeblikket ikke medlem av noen organisasjoner. Lag en organisasjon for å komme i gang.", + "componentsErrorNoMember": "Du er for øyeblikket ikke medlem av noen organisasjoner.", + "welcome": "Velkommen!", + "welcomeTo": "Velkommen til", + "componentsCreateOrg": "Lag en Organisasjon", + "componentsMember": "Du er {count, plural, =0 {ikke medlem av noen organisasjoner} =1 {medlem av en organisasjon} other {medlem av # organisasjoner}}.", + "componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", + "dismiss": "Avvis", + "componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", + "componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!", + "inviteErrorNotValid": "Beklager, men det ser ut som invitasjonen du prøver å bruke ikke har blitt akseptert eller ikke er gyldig lenger.", + "inviteErrorUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til, ikke er for denne brukeren.", + "inviteLoginUser": "Vennligst sjekk at du er logget inn som riktig bruker.", + "inviteErrorNoUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til ikke er for en bruker som eksisterer.", + "inviteCreateUser": "Vennligst opprett en konto først.", + "goHome": "Gå hjem", + "inviteLogInOtherUser": "Logg inn som en annen bruker", + "createAnAccount": "Lag konto", + "inviteNotAccepted": "Invitasjonen ikke akseptert", + "authCreateAccount": "Opprett en konto for å komme i gang", + "authNoAccount": "Har du ikke konto?", + "email": "E-post", + "password": "Passord", + "confirmPassword": "Bekreft Passord", + "createAccount": "Opprett Konto", + "viewSettings": "Vis Innstillinger", + "delete": "Slett", + "name": "Navn", + "online": "Online", + "offline": "Frakoblet", + "site": "Område", + "dataIn": "Data Inn", + "dataOut": "Data Ut", + "connectionType": "Tilkoblingstype", + "tunnelType": "Tunneltype", + "local": "Lokal", + "edit": "Rediger", + "siteConfirmDelete": "Bekreft Sletting av Område", + "siteDelete": "Slett Område", + "siteMessageRemove": "Når området slettes, vil det ikke lenger være tilgjengelig. Alle ressurser og mål assosiert med området vil også bli slettet.", + "siteMessageConfirm": "For å bekrefte, vennligst skriv inn navnet i området nedenfor.", + "siteQuestionRemove": "Er du sikker på at du vil slette området {selectedSite} fra organisasjonen?", + "siteManageSites": "Administrer Områder", + "siteDescription": "Tillat tilkobling til nettverket ditt gjennom sikre tunneler", + "siteCreate": "Opprett område", + "siteCreateDescription2": "Følg trinnene nedenfor for å opprette og koble til et nytt område", + "siteCreateDescription": "Opprett et nytt område for å begynne å koble til ressursene dine", + "close": "Lukk", + "siteErrorCreateKeyPair": "Nøkkelpar eller standardinnstillinger for område ikke funnet", + "siteErrorCreate": "Feil ved oppretting av område", + "siteErrorCreateDefaults": "Standardinnstillinger for område ikke funnet", + "method": "Metode", + "siteMethodDescription": "Slik eksponerer du tilkoblinger.", + "siteLearnNewt": "Lær hvordan du installerer Newt på systemet ditt", + "siteSeeConfigOnce": "Du kan kun se konfigurasjonen én gang.", + "siteLoadWGConfig": "Laster WireGuard-konfigurasjon...", + "siteDocker": "Utvid for detaljer om Docker-deployment", + "toggle": "Veksle", + "dockerCompose": "Docker Compose", + "dockerRun": "Docker Run", + "siteLearnLocal": "Lokale områder tunnelerer ikke, lær mer", + "siteConfirmCopy": "Jeg har kopiert konfigurasjonen", + "searchSitesProgress": "Søker i områder...", + "siteAdd": "Legg til område", + "siteInstallNewt": "Installer Newt", + "siteInstallNewtDescription": "Få Newt til å kjøre på systemet ditt", + "WgConfiguration": "WireGuard Konfigurasjon", + "WgConfigurationDescription": "Bruk følgende konfigurasjon for å koble til nettverket ditt", + "operatingSystem": "Operativsystem", + "commands": "Kommandoer", + "recommended": "Anbefalt", + "siteNewtDescription": "For den beste brukeropplevelsen, bruk Newt. Den bruker WireGuard i bakgrunnen og lar deg adressere dine private ressurser med deres LAN-adresse på ditt private nettverk fra Pangolin-dashbordet.", + "siteRunsInDocker": "Kjører i Docker", + "siteRunsInShell": "Kjører i skall på macOS, Linux og Windows", + "siteErrorDelete": "Feil ved sletting av området", + "siteErrorUpdate": "Klarte ikke å oppdatere området", + "siteErrorUpdateDescription": "En feil oppstod under oppdatering av området.", + "siteUpdated": "Område oppdatert", + "siteUpdatedDescription": "Området har blitt oppdatert.", + "siteGeneralDescription": "Konfigurer de generelle innstillingene for dette området", + "siteSettingDescription": "Konfigurer innstillingene for området ditt", + "siteSetting": "{siteName} Innstillinger", + "siteNewtTunnel": "Newt Tunnel (Anbefalt)", + "siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.", + "siteWg": "Grunnleggende WireGuard", + "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", + "siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.", + "siteSeeAll": "Se alle områder", + "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", + "siteNewtCredentials": "Newt påloggingsinformasjon", + "siteNewtCredentialsDescription": "Slik vil Newt autentisere seg mot serveren", + "siteCredentialsSave": "Lagre påloggingsinformasjonen din", + "siteCredentialsSaveDescription": "Du vil kun kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", + "siteInfo": "Områdeinformasjon", + "status": "Status", + "shareTitle": "Administrer delingslenker", + "shareDescription": "Opprett delbare lenker for å gi midlertidig eller permanent tilgang til ressursene dine", + "shareSearch": "Søk delingslenker...", + "shareCreate": "Opprett delingslenke", + "shareErrorDelete": "Klarte ikke å slette lenke", + "shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke", + "shareDeleted": "Lenke slettet", + "shareDeletedDescription": "Lenken har blitt slettet", + "shareTokenDescription": "Din tilgangsnøkkel kan sendes på to måter: som en query parameter eller i request headers. Disse må sendes fra klienten på hver forespørsel for autentisert tilgang.", + "accessToken": "Tilgangsnøkkel", + "usageExamples": "Brukseksempler", + "tokenId": "Token-ID", + "requestHeades": "Request Headers", + "queryParameter": "Query Parameter", + "importantNote": "Viktig merknad", + "shareImportantDescription": "Av sikkerhetsgrunner anbefales det å bruke headere fremfor query parametere der det er mulig, da query parametere kan logges i serverlogger eller nettleserhistorikk.", + "token": "Token", + "shareTokenSecurety": "Hold tilgangsnøkkelen ditt sikkert. Ikke del i offentlig tilgjengelige områder eller klientkode.", + "shareErrorFetchResource": "Klarte ikke å hente ressurser", + "shareErrorFetchResourceDescription": "En feil oppstod under henting av ressursene", + "shareErrorCreate": "Mislyktes med å opprette delingslenke", + "shareErrorCreateDescription": "Det oppsto en feil ved opprettelse av delingslenken", + "shareCreateDescription": "Alle med denne lenken får tilgang til ressursen", + "shareTitleOptional": "Tittel (valgfritt)", + "expireIn": "Utløper om", + "neverExpire": "Utløper aldri", + "shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.", + "shareSeeOnce": "Du får bare se denne lenken én gang. Pass på å kopiere den.", + "shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.", + "shareTokenUsage": "Se tilgangstokenbruk", + "createLink": "Opprett lenke", + "resourcesNotFound": "Ingen ressurser funnet", + "resourceSearch": "Søk i ressurser", + "openMenu": "Åpne meny", + "resource": "Ressurs", + "title": "Tittel", + "created": "Opprettet", + "expires": "Utløper", + "never": "Aldri", + "shareErrorSelectResource": "Vennligst velg en ressurs", + "resourceTitle": "Administrer Ressurser", + "resourceDescription": "Opprett sikre proxyer til dine private applikasjoner", + "resourcesSearch": "Søk i ressurser...", + "resourceAdd": "Legg til ressurs", + "resourceErrorDelte": "Feil ved sletting av ressurs", + "authentication": "Autentisering", + "protected": "Beskyttet", + "notProtected": "Ikke beskyttet", + "resourceMessageRemove": "Når den er fjernet, vil ressursen ikke lenger være tilgjengelig. Alle mål knyttet til ressursen vil også bli fjernet.", + "resourceMessageConfirm": "For å bekrefte, skriv inn navnet på ressursen nedenfor.", + "resourceQuestionRemove": "Er du sikker på at du vil fjerne ressursen {selectedResource} fra organisasjonen?", + "resourceHTTP": "HTTPS-ressurs", + "resourceHTTPDescription": "Proxy-forespørsler til appen din over HTTPS ved bruk av et underdomene eller grunndomene.", + "resourceRaw": "Rå TCP/UDP-ressurs", + "resourceRawDescription": "Proxyer forespørsler til appen din over TCP/UDP ved å bruke et portnummer.", + "resourceCreate": "Opprett ressurs", + "resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs", + "resourceSeeAll": "Se alle ressurser", + "resourceInfo": "Ressursinformasjon", + "resourceNameDescription": "Dette er visningsnavnet for ressursen.", + "siteSelect": "Velg område", + "siteSearch": "Søk i område", + "siteNotFound": "Ingen område funnet.", + "siteSelectionDescription": "Dette området vil gi tilkobling til ressursen.", + "resourceType": "Ressurstype", + "resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din", + "resourceHTTPSSettings": "HTTPS-innstillinger", + "resourceHTTPSSettingsDescription": "Konfigurer tilgang til ressursen din over HTTPS", + "domainType": "Domenetype", + "subdomain": "Underdomene", + "baseDomain": "Grunndomene", + "subdomnainDescription": "Underdomenet der ressursen din vil være tilgjengelig.", + "resourceRawSettings": "TCP/UDP-innstillinger", + "resourceRawSettingsDescription": "Konfigurer tilgang til ressursen din over TCP/UDP", + "protocol": "Protokoll", + "protocolSelect": "Velg en protokoll", + "resourcePortNumber": "Portnummer", + "resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.", + "cancel": "Avbryt", + "resourceConfig": "Konfigurasjonsutdrag", + "resourceConfigDescription": "Kopier og lim inn disse konfigurasjonsutdragene for å sette opp din TCP/UDP-ressurs", + "resourceAddEntrypoints": "Traefik: Legg til inngangspunkter", + "resourceExposePorts": "Gerbil: Eksponer Porter i Docker Compose", + "resourceLearnRaw": "Lær hvordan å konfigurere TCP/UDP-ressurser", + "resourceBack": "Tilbake til ressurser", + "resourceGoTo": "Gå til ressurs", + "resourceDelete": "Slett ressurs", + "resourceDeleteConfirm": "Bekreft sletting av ressurs", + "visibility": "Synlighet", + "enabled": "Aktivert", + "disabled": "Deaktivert", + "general": "Generelt", + "generalSettings": "Generelle innstillinger", + "proxy": "Proxy", + "rules": "Regler", + "resourceSettingDescription": "Konfigurer innstillingene på ressursen din", + "resourceSetting": "{resourceName} Innstillinger", + "alwaysAllow": "Alltid tillat", + "alwaysDeny": "Alltid avslå", + "orgSettingsDescription": "Konfigurer organisasjonens generelle innstillinger", + "orgGeneralSettings": "Organisasjonsinnstillinger", + "orgGeneralSettingsDescription": "Administrer dine organisasjonsdetaljer og konfigurasjon", + "saveGeneralSettings": "Lagre generelle innstillinger", + "saveSettings": "Lagre innstillinger", + "orgDangerZone": "Faresone", + "orgDangerZoneDescription": "Når du sletter denne organisasjonen er det ingen vei tilbake. Vennligst vær sikker.", + "orgDelete": "Slett organisasjon", + "orgDeleteConfirm": "Bekreft Sletting av Organisasjon", + "orgMessageRemove": "Denne handlingen er irreversibel og vil slette alle tilknyttede data.", + "orgMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på organisasjonen nedenfor.", + "orgQuestionRemove": "Er du sikker på at du vil fjerne organisasjonen {selectedOrg}?", + "orgUpdated": "Organisasjon oppdatert", + "orgUpdatedDescription": "Organisasjonen har blitt oppdatert.", + "orgErrorUpdate": "Kunne ikke oppdatere organisasjonen", + "orgErrorUpdateMessage": "En feil oppsto under oppdatering av organisasjonen.", + "orgErrorFetch": "Klarte ikke å hente organisasjoner", + "orgErrorFetchMessage": "Det oppstod en feil under opplisting av organisasjonene dine", + "orgErrorDelete": "Klarte ikke å slette organisasjon", + "orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.", + "orgDeleted": "Organisasjon slettet", + "orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.", + "orgMissing": "Organisasjons-ID Mangler", + "orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.", + "accessUsersManage": "Administrer brukere", + "accessUsersDescription": "Inviter brukere og gi dem roller for å administrere tilgang til organisasjonen din", + "accessUsersSearch": "Søk etter brukere...", + "accessUserCreate": "Opprett bruker", + "accessUserRemove": "Fjern bruker", + "username": "Brukernavn", + "identityProvider": "Identitetsleverandør", + "role": "Rolle", + "nameRequired": "Navn er påkrevd", + "accessRolesManage": "Administrer Roller", + "accessRolesDescription": "Konfigurer roller for å administrere tilgang til organisasjonen din", + "accessRolesSearch": "Søk etter roller...", + "accessRolesAdd": "Legg til rolle", + "accessRoleDelete": "Slett rolle", + "description": "Beskrivelse", + "inviteTitle": "Åpne invitasjoner", + "inviteDescription": "Administrer invitasjonene dine til andre brukere", + "inviteSearch": "Søk i invitasjoner...", + "minutes": "Minutter", + "hours": "Timer", + "days": "Dager", + "weeks": "Uker", + "months": "Måneder", + "years": "År", + "day": "{count, plural, en {# dag} other {# dager}}", + "apiKeysTitle": "API-nøkkel informasjon", + "apiKeysConfirmCopy2": "Du må bekrefte at du har kopiert API-nøkkelen.", + "apiKeysErrorCreate": "Feil ved oppretting av API-nøkkel", + "apiKeysErrorSetPermission": "Feil ved innstilling av tillatelser", + "apiKeysCreate": "Generer API-nøkkel", + "apiKeysCreateDescription": "Generer en ny API-nøkkel for din organisasjon", + "apiKeysGeneralSettings": "Tillatelser", + "apiKeysGeneralSettingsDescription": "Finn ut hva denne API-nøkkelen kan gjøre", + "apiKeysList": "Din API-nøkkel", + "apiKeysSave": "Lagre API-nøkkelen din", + "apiKeysSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", + "apiKeysInfo": "Din API-nøkkel er:", + "apiKeysConfirmCopy": "Jeg har kopiert API-nøkkelen", + "generate": "Generer", + "done": "Ferdig", + "apiKeysSeeAll": "Se alle API-nøkler", + "apiKeysPermissionsErrorLoadingActions": "Feil ved innlasting av API-nøkkel handlinger", + "apiKeysPermissionsErrorUpdate": "Feil ved innstilling av tillatelser", + "apiKeysPermissionsUpdated": "Tillatelser oppdatert", + "apiKeysPermissionsUpdatedDescription": "Tillatelsene har blitt oppdatert.", + "apiKeysPermissionsGeneralSettings": "Tillatelser", + "apiKeysPermissionsGeneralSettingsDescription": "Bestem hva denne API-nøkkelen kan gjøre", + "apiKeysPermissionsSave": "Lagre tillatelser", + "apiKeysPermissionsTitle": "Tillatelser", + "apiKeys": "API-nøkler", + "searchApiKeys": "Søk API-nøkler", + "apiKeysAdd": "Generer API-nøkkel", + "apiKeysErrorDelete": "Feil under sletting av API-nøkkel", + "apiKeysErrorDeleteMessage": "Feil ved sletting av API-nøkkel", + "apiKeysQuestionRemove": "Er du sikker på at du vil fjerne API-nøkkelen {selectedApiKey} fra organisasjonen?", + "apiKeysMessageRemove": "Når den er fjernet, vil API-nøkkelen ikke lenger kunne brukes.", + "apiKeysMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på API-nøkkelen nedenfor.", + "apiKeysDeleteConfirm": "Bekreft sletting av API-nøkkel", + "apiKeysDelete": "Slett API-nøkkel", + "apiKeysManage": "Administrer API-nøkler", + "apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API", + "apiKeysSettings": "{apiKeyName} Innstillinger", + "userTitle": "Administrer alle brukere", + "userDescription": "Vis og administrer alle brukere i systemet", + "userAbount": "Om brukeradministrasjon", + "userAbountDescription": "Denne tabellen viser alle rotbrukerobjekter i systemet. Hver bruker kan tilhøre flere organisasjoner. Å fjerne en bruker fra en organisasjon sletter ikke deres rotbrukerobjekt – de vil forbli i systemet. For å fullstendig fjerne en bruker fra systemet, må du slette deres rotbrukerobjekt ved å bruke slett-handlingen i denne tabellen.", + "userServer": "Serverbrukere", + "userSearch": "Søk serverbrukere...", + "userErrorDelete": "Feil ved sletting av bruker", + "userDeleteConfirm": "Bekreft sletting av bruker", + "userDeleteServer": "Slett bruker fra server", + "userMessageRemove": "Brukeren vil bli fjernet fra alle organisasjoner og vil bli fullstendig fjernet fra serveren.", + "userMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", + "userQuestionRemove": "Er du sikker på at du vil slette {selectedUser} permanent fra serveren?", + "licenseKey": "Lisensnøkkel", + "valid": "Gyldig", + "numberOfSites": "Antall områder", + "licenseKeySearch": "Søk lisensnøkler...", + "licenseKeyAdd": "Legg til lisensnøkkel", + "type": "Type", + "licenseKeyRequired": "Lisensnøkkel er påkrevd", + "licenseTermsAgree": "Du må godta lisensvilkårene", + "licenseErrorKeyLoad": "Feil ved lasting av lisensnøkler", + "licenseErrorKeyLoadDescription": "Det oppstod en feil ved lasting av lisensnøkler.", + "licenseErrorKeyDelete": "Kunne ikke slette lisensnøkkel", + "licenseErrorKeyDeleteDescription": "Det oppstod en feil ved sletting av lisensnøkkel.", + "licenseKeyDeleted": "Lisensnøkkel slettet", + "licenseKeyDeletedDescription": "Lisensnøkkelen har blitt slettet.", + "licenseErrorKeyActivate": "Aktivering av lisensnøkkel feilet", + "licenseErrorKeyActivateDescription": "Det oppstod en feil under aktivering av lisensnøkkelen.", + "licenseAbout": "Om Lisensiering", + "communityEdition": "Fellesskapsutgave", + "licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.", + "licenseKeyActivated": "Lisensnøkkel aktivert", + "licenseKeyActivatedDescription": "Lisensnøkkelen har blitt vellykket aktivert.", + "licenseErrorKeyRecheck": "En feil oppsto under verifisering av lisensnøkler", + "licenseErrorKeyRecheckDescription": "Det oppstod en feil under verifisering av lisensnøkler.", + "licenseErrorKeyRechecked": "Lisensnøkler verifisert", + "licenseErrorKeyRecheckedDescription": "Alle lisensnøkler er verifisert", + "licenseActivateKey": "Aktiver lisensnøkkel", + "licenseActivateKeyDescription": "Skriv inn en lisensnøkkel for å aktivere den.", + "licenseActivate": "Aktiver lisens", + "licenseAgreement": "Ved å krysse av denne boksen bekrefter du at du har lest og godtar lisensvilkårene som tilsvarer nivået tilknyttet lisensnøkkelen din.", + "fossorialLicense": "Vis Fossorial kommersiell lisens og abonnementsvilkår", + "licenseMessageRemove": "Dette vil fjerne lisensnøkkelen og alle tilknyttede tillatelser gitt av den.", + "licenseMessageConfirm": "For å bekrefte, vennligst skriv inn lisensnøkkelen nedenfor.", + "licenseQuestionRemove": "Er du sikker på at du vil slette lisensnøkkelen {selectedKey} ?", + "licenseKeyDelete": "Slett Lisensnøkkel", + "licenseKeyDeleteConfirm": "Bekreft sletting av lisensnøkkel", + "licenseTitle": "Behandle lisensstatus", + "licenseTitleDescription": "Se og administrer lisensnøkler i systemet", + "licenseHost": "Vertslisens", + "licenseHostDescription": "Behandle hovedlisensnøkkelen for verten.", + "licensedNot": "Ikke lisensiert", + "hostId": "Verts-ID", + "licenseReckeckAll": "Verifiser alle nøkler", + "licenseSiteUsage": "Område Bruk", + "licenseSiteUsageDecsription": "Vis antall områder som bruker denne lisensen.", + "licenseNoSiteLimit": "Det er ingen grense på antall områder som bruker en ulisensiert vert.", + "licensePurchase": "Kjøp lisens", + "licensePurchaseSites": "Kjøp flere områder", + "licenseSitesUsedMax": "{usedSites} av {maxSites} områder brukt", + "licenseSitesUsed": "{count, plural, =0 {# områder} en {# område} other {# områder}} i systemet.", + "licensePurchaseDescription": "Velg hvor mange områder du vil {selectedMode, select, license {kjøpe en lisens for. Du kan alltid legge til flere områder senere.} other {legge til din eksisterende lisens.}}", + "licenseFee": "Lisensavgift", + "licensePriceSite": "Pris per område", + "total": "Totalt", + "licenseContinuePayment": "Fortsett til betaling", + "pricingPage": "Pris oversikt", + "pricingPortal": "Se Kjøpsportal", + "licensePricingPage": "For de mest oppdaterte prisene og rabattene, vennligst besøk", + "invite": "Invitasjoner", + "inviteRegenerate": "Regenerer invitasjonen", + "inviteRegenerateDescription": "Tilbakekall tidligere invitasjon og opprette en ny", + "inviteRemove": "Fjern invitasjon", + "inviteRemoveError": "Mislyktes å fjerne invitasjon", + "inviteRemoveErrorDescription": "Det oppstod en feil under fjerning av invitasjonen.", + "inviteRemoved": "Invitasjon fjernet", + "inviteRemovedDescription": "Invitasjonen for {email} er fjernet.", + "inviteQuestionRemove": "Er du sikker på at du vil fjerne invitasjonen {email}?", + "inviteMessageRemove": "Når fjernet, vil denne invitasjonen ikke lenger være gyldig. Du kan alltid invitere brukeren på nytt senere.", + "inviteMessageConfirm": "For å bekrefte, vennligst tast inn invitasjonens e-postadresse nedenfor.", + "inviteQuestionRegenerate": "Er du sikker på at du vil generere invitasjonen på nytt for {email}? Dette vil ugyldiggjøre den forrige invitasjonen.", + "inviteRemoveConfirm": "Bekreft fjerning av invitasjon", + "inviteRegenerated": "Invitasjon fornyet", + "inviteSent": "En ny invitasjon er sendt til {email}.", + "inviteSentEmail": "Send e-postvarsel til brukeren", + "inviteGenerate": "En ny invitasjon er generert for {email}.", + "inviteDuplicateError": "Dupliser invitasjon", + "inviteDuplicateErrorDescription": "En invitasjon for denne brukeren eksisterer allerede.", + "inviteRateLimitError": "Forespørselsgrense overskredet", + "inviteRateLimitErrorDescription": "Du har overskredet grensen på 3 regenerasjoner per time. Prøv igjen senere.", + "inviteRegenerateError": "Kunne ikke regenerere invitasjon", + "inviteRegenerateErrorDescription": "Det oppsto en feil under regenerering av invitasjonen.", + "inviteValidityPeriod": "Gyldighetsperiode", + "inviteValidityPeriodSelect": "Velg gyldighetsperiode", + "inviteRegenerateMessage": "Invitasjonen er generert på nytt. Brukeren må gå til lenken nedenfor for å akseptere invitasjonen.", + "inviteRegenerateButton": "Regenerer", + "expiresAt": "Utløpstidspunkt", + "accessRoleUnknown": "Ukjent rolle", + "placeholder": "Plassholder", + "userErrorOrgRemove": "En feil oppsto under fjerning av bruker", + "userErrorOrgRemoveDescription": "Det oppstod en feil under fjerning av brukeren.", + "userOrgRemoved": "Bruker fjernet", + "userOrgRemovedDescription": "Brukeren {email} er fjernet fra organisasjonen.", + "userQuestionOrgRemove": "Er du sikker på at du vil fjerne {email} fra organisasjonen?", + "userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.", + "userMessageOrgConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", + "userRemoveOrgConfirm": "Bekreft fjerning av bruker", + "userRemoveOrg": "Fjern bruker fra organisasjon", + "users": "Brukere", + "accessRoleMember": "Medlem", + "accessRoleOwner": "Eier", + "userConfirmed": "Bekreftet", + "idpNameInternal": "Intern", + "emailInvalid": "Ugyldig e-postadresse", + "inviteValidityDuration": "Vennligst velg en varighet", + "accessRoleSelectPlease": "Vennligst velg en rolle", + "usernameRequired": "Brukernavn er påkrevd", + "idpSelectPlease": "Vennligst velg en identitetsleverandør", + "idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.", + "accessRoleErrorFetch": "En feil oppsto under henting av roller", + "accessRoleErrorFetchDescription": "En feil oppsto under henting av rollene", + "idpErrorFetch": "En feil oppsto under henting av identitetsleverandører", + "idpErrorFetchDescription": "En feil oppsto ved henting av identitetsleverandører", + "userErrorExists": "Bruker eksisterer allerede", + "userErrorExistsDescription": "Denne brukeren er allerede medlem av organisasjonen.", + "inviteError": "Kunne ikke invitere bruker", + "inviteErrorDescription": "En feil oppsto under invitering av brukeren", + "userInvited": "Bruker invitert", + "userInvitedDescription": "Brukeren er vellykket invitert.", + "userErrorCreate": "Kunne ikke opprette bruker", + "userErrorCreateDescription": "Det oppsto en feil under oppretting av brukeren", + "userCreated": "Bruker opprettet", + "userCreatedDescription": "Brukeren har blitt vellykket opprettet.", + "userTypeInternal": "Intern bruker", + "userTypeInternalDescription": "Inviter en bruker til å bli med i organisasjonen din direkte.", + "userTypeExternal": "Ekstern bruker", + "userTypeExternalDescription": "Opprett en bruker med en ekstern identitetsleverandør.", + "accessUserCreateDescription": "Følg stegene under for å opprette en ny bruker", + "userSeeAll": "Se alle brukere", + "userTypeTitle": "Brukertype", + "userTypeDescription": "Bestem hvordan du vil opprette brukeren", + "userSettings": "Brukerinformasjon", + "userSettingsDescription": "Skriv inn detaljene for den nye brukeren", + "inviteEmailSent": "Send invitasjonsepost til bruker", + "inviteValid": "Gyldig for", + "selectDuration": "Velg varighet", + "accessRoleSelect": "Velg rolle", + "inviteEmailSentDescription": "En e-post er sendt til brukeren med tilgangslenken nedenfor. De må åpne lenken for å akseptere invitasjonen.", + "inviteSentDescription": "Brukeren har blitt invitert. De må åpne lenken nedenfor for å godta invitasjonen.", + "inviteExpiresIn": "Invitasjonen utløper om {days, plural, en {# dag} other {# dager}}.", + "idpTitle": "Identitetsleverandør", + "idpSelect": "Velg identitetsleverandøren for den eksterne brukeren", + "idpNotConfigured": "Ingen identitetsleverandører er konfigurert. Vennligst konfigurer en identitetsleverandør før du oppretter eksterne brukere.", + "usernameUniq": "Dette må matche det unike brukernavnet som finnes i den valgte identitetsleverandøren.", + "emailOptional": "E-post (Valgfritt)", + "nameOptional": "Navn (valgfritt)", + "accessControls": "Tilgangskontroller", + "userDescription2": "Administrer innstillingene for denne brukeren", + "accessRoleErrorAdd": "Kunne ikke legge til bruker i rolle", + "accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.", + "userSaved": "Bruker lagret", + "userSavedDescription": "Brukeren har blitt oppdatert.", + "accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen", + "accessControlsSubmit": "Lagre tilgangskontroller", + "roles": "Roller", + "accessUsersRoles": "Administrer brukere og roller", + "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen din.", + "key": "Nøkkel", + "createdAt": "Opprettet", + "proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.", + "proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.", + "proxyEnableSSL": "Aktiver SSL (https)", + "targetErrorFetch": "Kunne ikke hente mål", + "targetErrorFetchDescription": "Det oppsto en feil under henting av mål", + "siteErrorFetch": "Klarte ikke å hente ressurs", + "siteErrorFetchDescription": "Det oppstod en feil under henting av ressurs", + "targetErrorDuplicate": "Dupliser mål", + "targetErrorDuplicateDescription": "Et mål med disse innstillingene finnes allerede", + "targetWireGuardErrorInvalidIp": "Ugyldig mål-IP", + "targetWireGuardErrorInvalidIpDescription": "Mål-IP må være i områdets undernett.", + "targetsUpdated": "Mål oppdatert", + "targetsUpdatedDescription": "Mål og innstillinger oppdatert vellykket", + "targetsErrorUpdate": "Feilet å oppdatere mål", + "targetsErrorUpdateDescription": "En feil oppsto under oppdatering av mål", + "targetTlsUpdate": "TLS-innstillinger oppdatert", + "targetTlsUpdateDescription": "Dine TLS-innstillinger er oppdatert", + "targetErrorTlsUpdate": "Feilet under oppdatering av TLS-innstillinger", + "targetErrorTlsUpdateDescription": "Det oppstod en feil under oppdatering av TLS-innstillinger", + "proxyUpdated": "Proxy-innstillinger oppdatert", + "proxyUpdatedDescription": "Proxy-innstillingene dine er oppdatert", + "proxyErrorUpdate": "En feil oppsto under oppdatering av proxyinnstillinger", + "proxyErrorUpdateDescription": "En feil oppsto under oppdatering av proxyinnstillinger", + "targetAddr": "IP / vertsnavn", + "targetPort": "Port", + "targetProtocol": "Protokoll", + "targetTlsSettings": "Sikker tilkoblings-konfigurasjon", + "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din", + "targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger", + "targetTlsSni": "TLS Servernavn (SNI)", + "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", + "targetTlsSubmit": "Lagre innstillinger", + "targets": "Målkonfigurasjon", + "targetsDescription": "Sett opp mål for å rute trafikk til tjenestene dine", + "targetStickySessions": "Aktiver klebrige sesjoner", + "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", + "methodSelect": "Velg metode", + "targetSubmit": "Legg til mål", + "targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.", + "targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.", + "targetsSubmit": "Lagre mål", + "proxyAdditional": "Ytterligere Proxy-innstillinger", + "proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger", + "proxyCustomHeader": "Tilpasset verts-header", + "proxyCustomHeaderDescription": "Verts-header som skal settes ved videresending av forespørsler. La stå tom for å bruke standardinnstillingen.", + "proxyAdditionalSubmit": "Lagre proxy-innstillinger", + "subnetMaskErrorInvalid": "Ugyldig subnettmaske. Må være mellom 0 og 32.", + "ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat", + "ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet", + "path": "Sti", + "ipAddressRange": "IP-område", + "rulesErrorFetch": "Klarte ikke å hente regler", + "rulesErrorFetchDescription": "Det oppsto en feil under henting av regler", + "rulesErrorDuplicate": "Duplisert regel", + "rulesErrorDuplicateDescription": "En regel med disse innstillingene finnes allerede", + "rulesErrorInvalidIpAddressRange": "Ugyldig CIDR", + "rulesErrorInvalidIpAddressRangeDescription": "Vennligst skriv inn en gyldig CIDR-verdi", + "rulesErrorInvalidUrl": "Ugyldig URL-sti", + "rulesErrorInvalidUrlDescription": "Skriv inn en gyldig verdi for URL-sti", + "rulesErrorInvalidIpAddress": "Ugyldig IP", + "rulesErrorInvalidIpAddressDescription": "Skriv inn en gyldig IP-adresse", + "rulesErrorUpdate": "Kunne ikke oppdatere regler", + "rulesErrorUpdateDescription": "Det oppsto en feil under oppdatering av regler", + "rulesUpdated": "Aktiver Regler", + "rulesUpdatedDescription": "Regelevalueringen har blitt oppdatert", + "rulesMatchIpAddressRangeDescription": "Angi en adresse i CIDR-format (f.eks., 103.21.244.0/22)", + "rulesMatchIpAddress": "Angi en IP-adresse (f.eks. 103.21.244.12)", + "rulesMatchUrl": "Skriv inn en URL-sti eller et mønster (f.eks. /api/v1/todos eller /api/v1/*)", + "rulesErrorInvalidPriority": "Ugyldig prioritet", + "rulesErrorInvalidPriorityDescription": "Vennligst skriv inn en gyldig prioritet", + "rulesErrorDuplicatePriority": "Dupliserte prioriteringer", + "rulesErrorDuplicatePriorityDescription": "Vennligst angi unike prioriteringer", + "ruleUpdated": "Regler oppdatert", + "ruleUpdatedDescription": "Reglene er oppdatert", + "ruleErrorUpdate": "Operasjon mislyktes", + "ruleErrorUpdateDescription": "En feil oppsto under lagringsoperasjonen", + "rulesPriority": "Prioritet", + "rulesAction": "Handling", + "rulesMatchType": "Trefftype", + "value": "Verdi", + "rulesAbout": "Om regler", + "rulesAboutDescription": "Regler lar deg kontrollere tilgang til din ressurs basert på et sett med kriterier. Du kan opprette regler for å tillate eller nekte tilgang basert på IP-adresse eller URL-sti.", + "rulesActions": "Handlinger", + "rulesActionAlwaysAllow": "Alltid Tillat: Omgå alle autentiserings metoder", + "rulesActionAlwaysDeny": "Alltid Nekt: Blokker alle forespørsler; ingen autentisering kan forsøkes", + "rulesMatchCriteria": "Samsvarende kriterier", + "rulesMatchCriteriaIpAddress": "Samsvar med en spesifikk IP-adresse", + "rulesMatchCriteriaIpAddressRange": "Samsvar et IP-adresseområde i CIDR-notasjon", + "rulesMatchCriteriaUrl": "Match en URL-sti eller et mønster", + "rulesEnable": "Aktiver regler", + "rulesEnableDescription": "Aktiver eller deaktiver regelvurdering for denne ressursen", + "rulesResource": "Konfigurasjon av ressursregler", + "rulesResourceDescription": "Konfigurere regler for tilgangskontroll til ressursen din", + "ruleSubmit": "Legg til regel", + "rulesNoOne": "Ingen regler. Legg til en regel ved å bruke skjemaet.", + "rulesOrder": "Regler evalueres etter prioritet i stigende rekkefølge.", + "rulesSubmit": "Lagre regler", + "resourceErrorCreate": "Feil under oppretting av ressurs", + "resourceErrorCreateDescription": "Det oppstod en feil under oppretting av ressursen", + "resourceErrorCreateMessage": "Feil ved oppretting av ressurs:", + "resourceErrorCreateMessageDescription": "En uventet feil oppstod", + "sitesErrorFetch": "Feil ved henting av områder", + "sitesErrorFetchDescription": "En feil oppstod ved henting av områdene", + "domainsErrorFetch": "Kunne ikke hente domener", + "domainsErrorFetchDescription": "Det oppsto en feil under henting av domenene", + "none": "Ingen", + "unknown": "Ukjent", + "resources": "Ressurser", + "resourcesDescription": "Ressurser er proxyer for applikasjoner som kjører på ditt private nettverk. Opprett en ressurs for enhver HTTP/HTTPS- eller rå TCP/UDP-tjeneste på ditt private nettverk. Hver ressurs må kobles til et område for å muliggjøre privat, sikker tilkobling gjennom en kryptert WireGuard-tunnel.", + "resourcesWireGuardConnect": "Sikker tilkobling med WireGuard-kryptering", + "resourcesMultipleAuthenticationMethods": "Konfigurer flere autentiseringsmetoder", + "resourcesUsersRolesAccess": "Bruker- og rollebasert tilgangskontroll", + "resourcesErrorUpdate": "Feilet å slå av/på ressurs", + "resourcesErrorUpdateDescription": "En feil oppstod under oppdatering av ressursen", + "access": "Tilgang", + "shareLink": "{resource} Del Lenke", + "resourceSelect": "Velg ressurs", + "shareLinks": "Del lenker", + "share": "Delbare lenker", + "shareDescription2": "Opprett delbare lenker til ressursene dine. Lenker gir midlertidig eller ubegrenset tilgang til ressursen din. Du kan konfigurere utløpsvarigheten for lenken når du oppretter den.", + "shareEasyCreate": "Enkelt å lage og dele", + "shareConfigurableExpirationDuration": "Konfigurerbar utløpsvarighet", + "shareSecureAndRevocable": "Sikker og tilbakekallbar", + "nameMin": "Navn må være minst {len} tegn.", + "nameMax": "Navn kan ikke være lengre enn {len} tegn.", + "sitesConfirmCopy": "Vennligst bekreft at du har kopiert konfigurasjonen.", + "unknownCommand": "Ukjent kommando", + "newtErrorFetchReleases": "Feilet å hente utgivelsesinfo: {err}", + "newtErrorFetchLatest": "Feil ved henting av siste utgivelse: {err}", + "newtEndpoint": "Newt endepunkt", + "newtId": "Newt-ID", + "newtSecretKey": "Newt hemmelig nøkkel", + "architecture": "Arkitektur", + "sites": "Områder", + "siteWgAnyClients": "Bruk en hvilken som helst WireGuard-klient for å koble til. Du må adressere dine interne ressurser ved å bruke peer-IP-en.", + "siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter", + "siteWgManualConfigurationRequired": "Manuell konfigurasjon påkrevd", + "userErrorNotAdminOrOwner": "Bruker er ikke administrator eller eier", + "pangolinSettings": "Innstillinger - Pangolin", + "accessRoleYour": "Din rolle:", + "accessRoleSelect2": "Velg en rolle", + "accessUserSelect": "Velg en bruker", + "otpEmailEnter": "Skriv inn én e-post", + "otpEmailEnterDescription": "Trykk enter for å legge til en e-post etter å ha tastet den inn i tekstfeltet.", + "otpEmailErrorInvalid": "Ugyldig e-postadresse. Jokertegnet (*) må være hele lokaldelen.", + "otpEmailSmtpRequired": "SMTP påkrevd", + "otpEmailSmtpRequiredDescription": "SMTP må være aktivert på serveren for å bruke engangspassord-autentisering.", + "otpEmailTitle": "Engangspassord", + "otpEmailTitleDescription": "Krev e-postbasert autentisering for ressurstilgang", + "otpEmailWhitelist": "E-post-hviteliste", + "otpEmailWhitelistList": "Hvitlistede e-poster", + "otpEmailWhitelistListDescription": "Kun brukere med disse e-postadressene vil ha tilgang til denne ressursen. De vil bli bedt om å skrive inn et engangspassord sendt til e-posten deres. Jokertegn (*@example.com) kan brukes for å tillate enhver e-postadresse fra et domene.", + "otpEmailWhitelistSave": "Lagre hvitliste", + "passwordAdd": "Legg til passord", + "passwordRemove": "Fjern passord", + "pincodeAdd": "Legg til PIN-kode", + "pincodeRemove": "Fjern PIN-kode", + "resourceAuthMethods": "Autentiseringsmetoder", + "resourceAuthMethodsDescriptions": "Tillat tilgang til ressursen via ytterligere autentiseringsmetoder", + "resourceAuthSettingsSave": "Lagret vellykket", + "resourceAuthSettingsSaveDescription": "Autentiseringsinnstillinger er lagret", + "resourceErrorAuthFetch": "Kunne ikke hente data", + "resourceErrorAuthFetchDescription": "Det oppstod en feil ved henting av data", + "resourceErrorPasswordRemove": "Feil ved fjerning av passord for ressurs", + "resourceErrorPasswordRemoveDescription": "Det oppstod en feil ved fjerning av ressurspassordet", + "resourceErrorPasswordSetup": "Feil ved innstilling av ressurspassord", + "resourceErrorPasswordSetupDescription": "Det oppstod en feil ved innstilling av ressurspassordet", + "resourceErrorPincodeRemove": "Feil ved fjerning av ressurs-PIN-koden", + "resourceErrorPincodeRemoveDescription": "Det oppstod en feil under fjerning av ressurs-pinkoden", + "resourceErrorPincodeSetup": "Feil ved innstilling av ressurs-PIN-kode", + "resourceErrorPincodeSetupDescription": "Det oppstod en feil under innstilling av ressursens PIN-kode", + "resourceErrorUsersRolesSave": "Klarte ikke å sette roller", + "resourceErrorUsersRolesSaveDescription": "En feil oppstod ved innstilling av rollene", + "resourceErrorWhitelistSave": "Feilet å lagre hvitliste", + "resourceErrorWhitelistSaveDescription": "Det oppstod en feil under lagring av hvitlisten", + "resourcePasswordSubmit": "Aktiver passordbeskyttelse", + "resourcePasswordProtection": "Passordbeskyttelse {status}", + "resourcePasswordRemove": "Ressurspassord fjernet", + "resourcePasswordRemoveDescription": "Fjerning av ressurspassordet var vellykket", + "resourcePasswordSetup": "Ressurspassord satt", + "resourcePasswordSetupDescription": "Ressurspassordet har blitt vellykket satt", + "resourcePasswordSetupTitle": "Angi passord", + "resourcePasswordSetupTitleDescription": "Sett et passord for å beskytte denne ressursen", + "resourcePincode": "PIN-kode", + "resourcePincodeSubmit": "Aktiver PIN-kodebeskyttelse", + "resourcePincodeProtection": "PIN-kodebeskyttelse {status}", + "resourcePincodeRemove": "Ressurs PIN-kode fjernet", + "resourcePincodeRemoveDescription": "Ressurspassordet ble fjernet", + "resourcePincodeSetup": "Ressurs PIN-kode satt", + "resourcePincodeSetupDescription": "Ressurs PIN-kode er satt vellykket", + "resourcePincodeSetupTitle": "Angi PIN-kode", + "resourcePincodeSetupTitleDescription": "Sett en pinkode for å beskytte denne ressursen", + "resourceRoleDescription": "Administratorer har alltid tilgang til denne ressursen.", + "resourceUsersRoles": "Brukere og Roller", + "resourceUsersRolesDescription": "Konfigurer hvilke brukere og roller som har tilgang til denne ressursen", + "resourceUsersRolesSubmit": "Lagre brukere og roller", + "resourceWhitelistSave": "Lagring vellykket", + "resourceWhitelistSaveDescription": "Hvitlisteinnstillinger er lagret", + "ssoUse": "Bruk plattform SSO", + "ssoUseDescription": "Eksisterende brukere trenger kun å logge på én gang for alle ressurser som har dette aktivert.", + "proxyErrorInvalidPort": "Ugyldig portnummer", + "subdomainErrorInvalid": "Ugyldig underdomene", + "domainErrorFetch": "Feil ved henting av domener", + "domainErrorFetchDescription": "Det oppstod en feil ved henting av domenene", + "resourceErrorUpdate": "Mislyktes å oppdatere ressurs", + "resourceErrorUpdateDescription": "Det oppstod en feil under oppdatering av ressursen", + "resourceUpdated": "Ressurs oppdatert", + "resourceUpdatedDescription": "Ressursen er oppdatert vellykket", + "resourceErrorTransfer": "Klarte ikke å overføre ressurs", + "resourceErrorTransferDescription": "En feil oppsto under overføring av ressursen", + "resourceTransferred": "Ressurs overført", + "resourceTransferredDescription": "Ressursen er overført vellykket.", + "resourceErrorToggle": "Feilet å veksle ressurs", + "resourceErrorToggleDescription": "Det oppstod en feil ved oppdatering av ressursen", + "resourceVisibilityTitle": "Synlighet", + "resourceVisibilityTitleDescription": "Fullstendig aktiver eller deaktiver ressursynlighet", + "resourceGeneral": "Generelle innstillinger", + "resourceGeneralDescription": "Konfigurer de generelle innstillingene for denne ressursen", + "resourceEnable": "Aktiver ressurs", + "resourceTransfer": "Overfør Ressurs", + "resourceTransferDescription": "Overfør denne ressursen til et annet område", + "resourceTransferSubmit": "Overfør ressurs", + "siteDestination": "Destinasjonsområde", + "searchSites": "Søk områder", + "accessRoleCreate": "Opprett rolle", + "accessRoleCreateDescription": "Opprett en ny rolle for å gruppere brukere og administrere deres tillatelser.", + "accessRoleCreateSubmit": "Opprett rolle", + "accessRoleCreated": "Rolle opprettet", + "accessRoleCreatedDescription": "Rollen er vellykket opprettet.", + "accessRoleErrorCreate": "Klarte ikke å opprette rolle", + "accessRoleErrorCreateDescription": "Det oppstod en feil under opprettelse av rollen.", + "accessRoleErrorNewRequired": "Ny rolle kreves", + "accessRoleErrorRemove": "Kunne ikke fjerne rolle", + "accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.", + "accessRoleName": "Rollenavn", + "accessRoleQuestionRemove": "Du er i ferd med å slette rollen {name}. Du kan ikke angre denne handlingen.", + "accessRoleRemove": "Fjern Rolle", + "accessRoleRemoveDescription": "Fjern en rolle fra organisasjonen", + "accessRoleRemoveSubmit": "Fjern Rolle", + "accessRoleRemoved": "Rolle fjernet", + "accessRoleRemovedDescription": "Rollen er vellykket fjernet.", + "accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.", + "manage": "Administrer", + "sitesNotFound": "Ingen områder funnet.", + "pangolinServerAdmin": "Server Admin - Pangolin", + "licenseTierProfessional": "Profesjonell lisens", + "licenseTierEnterprise": "Bedriftslisens", + "licenseTierCommercial": "Kommersiell lisens", + "licensed": "Lisensiert", + "yes": "Ja", + "no": "Nei", + "sitesAdditional": "Ytterligere områder", + "licenseKeys": "Lisensnøkler", + "sitestCountDecrease": "Reduser antall områder", + "sitestCountIncrease": "Øk antall områder", + "idpManage": "Administrer Identitetsleverandører", + "idpManageDescription": "Vis og administrer identitetsleverandører i systemet", + "idpDeletedDescription": "Identitetsleverandør slettet vellykket", + "idpOidc": "OAuth2/OIDC", + "idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren {name} permanent?", + "idpMessageRemove": "Dette vil fjerne identitetsleverandøren og alle tilhørende konfigurasjoner. Brukere som autentiserer seg via denne leverandøren vil ikke lenger kunne logge inn.", + "idpMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på identitetsleverandøren nedenfor.", + "idpConfirmDelete": "Bekreft Sletting av Identitetsleverandør", + "idpDelete": "Slett identitetsleverandør", + "idp": "Identitetsleverandører", + "idpSearch": "Søk identitetsleverandører...", + "idpAdd": "Legg til Identitetsleverandør", + "idpClientIdRequired": "Klient-ID er påkrevd.", + "idpClientSecretRequired": "Klienthemmelighet er påkrevd.", + "idpErrorAuthUrlInvalid": "Autentiserings-URL må være en gyldig URL.", + "idpErrorTokenUrlInvalid": "Token-URL må være en gyldig URL.", + "idpPathRequired": "Identifikatorbane er påkrevd.", + "idpScopeRequired": "Omfang kreves.", + "idpOidcDescription": "Konfigurer en OpenID Connect identitetsleverandør", + "idpCreatedDescription": "Identitetsleverandør opprettet vellykket.", + "idpCreate": "Opprett identitetsleverandør", + "idpCreateDescription": "Konfigurer en ny identitetsleverandør for brukerautentisering", + "idpSeeAll": "Se alle identitetsleverandører", + "idpSettingsDescription": "Konfigurer grunnleggende informasjon for din identitetsleverandør", + "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", + "idpAutoProvisionUsers": "Automatisk brukerklargjøring", + "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", + "licenseBadge": "Profesjonell", + "idpType": "Leverandørtype", + "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", + "idpOidcConfigure": "OAuth2/OIDC-konfigurasjon", + "idpOidcConfigureDescription": "Konfigurer OAuth2/OIDC-leverandørens endepunkter og legitimasjon", + "idpClientId": "Klient-ID", + "idpClientIdDescription": "OAuth2-klient-ID-en fra identitetsleverandøren din", + "idpClientSecret": "Klienthemmelighet", + "idpClientSecretDescription": "OAuth2-klienthemmeligheten fra din identitetsleverandør", + "idpAuthUrl": "Autorisasjons-URL", + "idpAuthUrlDescription": "OAuth2 autorisasjonsendepunkt URL", + "idpTokenUrl": "Token-URL", + "idpTokenUrlDescription": "OAuth2-tokenendepunkt-URL", + "idpOidcConfigureAlert": "Viktig informasjon", + "idpOidcConfigureAlertDescription": "Etter at du har opprettet identitetsleverandøren, må du konfigurere callback-URL-en i identitetsleverandørens innstillinger. Callback-URL-en blir oppgitt etter vellykket opprettelse.", + "idpToken": "Token-konfigurasjon", + "idpTokenDescription": "Konfigurer hvordan brukerinformasjon trekkes ut fra ID-tokenet", + "idpJmespathAbout": "Om JMESPath", + "idpJmespathAboutDescription": "Stiene nedenfor bruker JMESPath-syntaks for å hente ut verdier fra ID-tokenet.", + "idpJmespathAboutDescriptionLink": "Lær mer om JMESPath", + "idpJmespathLabel": "Identifikatorsti", + "idpJmespathLabelDescription": "Stien til brukeridentifikatoren i ID-tokenet", + "idpJmespathEmailPathOptional": "E-poststi (Valgfritt)", + "idpJmespathEmailPathOptionalDescription": "Stien til brukerens e-postadresse i ID-tokenet", + "idpJmespathNamePathOptional": "Navn Sti (Valgfritt)", + "idpJmespathNamePathOptionalDescription": "Stien til brukerens navn i ID-tokenet", + "idpOidcConfigureScopes": "Omfang", + "idpOidcConfigureScopesDescription": "Mellomromseparert liste over OAuth2-omfang å be om", + "idpSubmit": "Opprett identitetsleverandør", + "orgPolicies": "Organisasjonsretningslinjer", + "idpSettings": "{idpName} Innstillinger", + "idpCreateSettingsDescription": "Konfigurer innstillingene for din identitetsleverandør", + "roleMapping": "Rolletilordning", + "orgMapping": "Organisasjon Kartlegging", + "orgPoliciesSearch": "Søk i organisasjonens retningslinjer...", + "orgPoliciesAdd": "Legg til organisasjonspolicy", + "orgRequired": "Organisasjon er påkrevd", + "error": "Feil", + "success": "Suksess", + "orgPolicyAddedDescription": "Policy vellykket lagt til", + "orgPolicyUpdatedDescription": "Policyen er vellykket oppdatert", + "orgPolicyDeletedDescription": "Policy slettet vellykket", + "defaultMappingsUpdatedDescription": "Standardtilordninger oppdatert vellykket", + "orgPoliciesAbout": "Om organisasjonens retningslinjer", + "orgPoliciesAboutDescription": "Organisasjonspolicyer brukes til å kontrollere tilgang til organisasjoner basert på brukerens ID-token. Du kan spesifisere JMESPath-uttrykk for å trekke ut rolle- og organisasjonsinformasjon fra ID-tokenet.", + "orgPoliciesAboutDescriptionLink": "Se dokumentasjon, for mer informasjon.", + "defaultMappingsOptional": "Standard Tilordninger (Valgfritt)", + "defaultMappingsOptionalDescription": "Standardtilordningene brukes når det ikke er definert en organisasjonspolicy for en organisasjon. Du kan spesifisere standard rolle- og organisasjonstilordninger som det kan falles tilbake på her.", + "defaultMappingsRole": "Standard rolletilordning", + "defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.", + "defaultMappingsOrg": "Standard organisasjonstilordning", + "defaultMappingsOrgDescription": "Dette uttrykket må returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.", + "defaultMappingsSubmit": "Lagre standard tilordninger", + "orgPoliciesEdit": "Rediger Organisasjonspolicy", + "org": "Organisasjon", + "orgSelect": "Velg organisasjon", + "orgSearch": "Søk organisasjon", + "orgNotFound": "Ingen organisasjon funnet.", + "roleMappingPathOptional": "Rollekartleggingssti (Valgfritt)", + "orgMappingPathOptional": "Organisasjonstilordningssti (Valgfritt)", + "orgPolicyUpdate": "Oppdater policy", + "orgPolicyAdd": "Legg til policy", + "orgPolicyConfig": "Konfigurer tilgang for en organisasjon", + "idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert", + "redirectUrl": "Omdirigerings-URL", + "redirectUrlAbout": "Om omdirigerings-URL", + "redirectUrlAboutDescription": "Dette er URL-en som brukere vil bli omdirigert til etter autentisering. Du må konfigurere denne URL-en i innstillingene for identitetsleverandøren din.", + "pangolinAuth": "Autentisering - Pangolin", + "verificationCodeLengthRequirements": "Din verifiseringskode må være 8 tegn.", + "errorOccurred": "Det oppstod en feil", + "emailErrorVerify": "Kunne ikke verifisere e-post:", + "emailVerified": "E-posten er bekreftet! Omdirigerer deg...", + "verificationCodeErrorResend": "Kunne ikke sende bekreftelseskode på nytt:", + "verificationCodeResend": "Bekreftelseskode sendt på nytt", + "verificationCodeResendDescription": "Vi har sendt en ny bekreftelseskode til e-postadressen din. Vennligst sjekk innboksen din.", + "emailVerify": "Verifiser e-post", + "emailVerifyDescription": "Skriv inn bekreftelseskoden sendt til e-postadressen din.", + "verificationCode": "Verifiseringskode", + "verificationCodeEmailSent": "Vi har sendt en bekreftelseskode til e-postadressen din.", + "submit": "Send inn", + "emailVerifyResendProgress": "Sender på nytt...", + "emailVerifyResend": "Har du ikke mottatt en kode? Klikk her for å sende på nytt", + "passwordNotMatch": "Passordene stemmer ikke", + "signupError": "Det oppsto en feil ved registrering", + "pangolinLogoAlt": "Pangolin Logo", + "inviteAlready": "Ser ut til at du har blitt invitert!", + "inviteAlreadyDescription": "For å godta invitasjonen, må du logge inn eller opprette en konto.", + "signupQuestion": "Har du allerede en konto?", + "login": "Logg inn", + "resourceNotFound": "Ressurs ikke funnet", + "resourceNotFoundDescription": "Ressursen du prøver å få tilgang til eksisterer ikke.", + "pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer", + "pincodeRequirementsChars": "PIN må kun inneholde tall", + "passwordRequirementsLength": "Passord må være minst 1 tegn langt", + "otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.", + "otpEmailSent": "OTP sendt", + "otpEmailSentDescription": "En OTP er sendt til din e-post", + "otpEmailErrorAuthenticate": "Mislyktes å autentisere med e-post", + "pincodeErrorAuthenticate": "Kunne ikke autentisere med pinkode", + "passwordErrorAuthenticate": "Kunne ikke autentisere med passord", + "poweredBy": "Drevet av", + "authenticationRequired": "Autentisering påkrevd", + "authenticationMethodChoose": "Velg din foretrukne metode for å få tilgang til {name}", + "authenticationRequest": "Du må autentisere deg for å få tilgang til {name}", + "user": "Bruker", + "pincodeInput": "6-sifret PIN-kode", + "pincodeSubmit": "Logg inn med PIN", + "passwordSubmit": "Logg inn med passord", + "otpEmailDescription": "En engangskode vil bli sendt til denne e-posten.", + "otpEmailSend": "Send engangskode", + "otpEmail": "Engangspassord (OTP)", + "otpEmailSubmit": "Send inn OTP", + "backToEmail": "Tilbake til E-post", + "noSupportKey": "Serveren kjører uten en supporterlisens. Vurder å støtte prosjektet!", + "accessDenied": "Tilgang nektet", + "accessDeniedDescription": "Du har ikke tilgang til denne ressursen. Hvis dette er en feil, vennligst kontakt administratoren.", + "accessTokenError": "Feil ved sjekk av tilgangstoken", + "accessGranted": "Tilgang gitt", + "accessUrlInvalid": "Ugyldig tilgangs-URL", + "accessGrantedDescription": "Du har fått tilgang til denne ressursen. Omdirigerer deg...", + "accessUrlInvalidDescription": "Denne delings-URL-en er ugyldig. Vennligst kontakt ressurseieren for en ny URL.", + "tokenInvalid": "Ugyldig token", + "pincodeInvalid": "Ugyldig kode", + "passwordErrorRequestReset": "Forespørsel om tilbakestilling mislyktes", + + "passwordErrorReset": "Klarte ikke å tilbakestille passord:", + "passwordResetSuccess": "Passordet er tilbakestilt! Går tilbake til innlogging...", + "passwordReset": "Tilbakestill passord", + "passwordResetDescription": "Følg stegene for å tilbakestille passordet ditt", + "passwordResetSent": "Vi sender en kode for tilbakestilling av passord til denne e-postadressen.", + "passwordResetCode": "Tilbakestillingskode", + "passwordResetCodeDescription": "Sjekk e-posten din for tilbakestillingskoden.", + "passwordNew": "Nytt passord", + "passwordNewConfirm": "Bekreft nytt passord", + "pincodeAuth": "Autentiseringskode", + "pincodeSubmit2": "Send inn kode", + "passwordResetSubmit": "Be om tilbakestilling", + "passwordBack": "Tilbake til passord", + "loginBack": "Gå tilbake til innlogging", + "signup": "Registrer deg", + "loginStart": "Logg inn for å komme i gang", + "idpOidcTokenValidating": "Validerer OIDC-token", + "idpOidcTokenResponse": "Valider OIDC-tokensvar", + "idpErrorOidcTokenValidating": "Feil ved validering av OIDC-token", + "idpConnectingTo": "Kobler til {name}", + "idpConnectingToDescription": "Validerer identiteten din", + "idpConnectingToProcess": "Kobler til...", + "idpConnectingToFinished": "Tilkoblet", + "idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.", + "idpErrorNotFound": "IdP ikke funnet", + "inviteInvalid": "Ugyldig invitasjon", + "inviteInvalidDescription": "Invitasjonslenken er ugyldig.", + "inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren", + "inviteErrorUserNotExists": "Brukeren eksisterer ikke. Vennligst opprett en konto først.", + "inviteErrorLoginRequired": "Du må være logget inn for å godta en invitasjon", + "inviteErrorExpired": "Invitasjonen kan ha utløpt", + "inviteErrorRevoked": "Invitasjonen kan ha blitt trukket tilbake", + "inviteErrorTypo": "Det kan være en skrivefeil i invitasjonslenken", + "pangolinSetup": "Oppsett - Pangolin", + "orgNameRequired": "Organisasjonsnavn er påkrevd", + "orgIdRequired": "Organisasjons-ID er påkrevd", + "orgErrorCreate": "En feil oppstod under oppretting av organisasjon", + "pageNotFound": "Siden ble ikke funnet", + "pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.", + "overview": "Oversikt", + "home": "Hjem", + "accessControl": "Tilgangskontroll", + "settings": "Innstillinger", + "usersAll": "Alle brukere", + "license": "Lisens", + "pangolinDashboard": "Dashbord - Pangolin", + "noResults": "Ingen resultater funnet.", + "terabytes": "{count} TB", + "gigabytes": "{count} GB", + "megabytes": "{count} MB", + "tagsEntered": "Inntastede tagger", + "tagsEnteredDescription": "Dette er taggene du har tastet inn.", + "tagsWarnCannotBeLessThanZero": "maxTags og minTags kan ikke være mindre enn 0", + "tagsWarnNotAllowedAutocompleteOptions": "Tagg ikke tillatt i henhold til autofullfør-alternativer", + "tagsWarnInvalid": "Ugyldig tagg i henhold til validateTag", + "tagWarnTooShort": "Tagg {tagText} er for kort", + "tagWarnTooLong": "Tagg {tagText} er for lang", + "tagsWarnReachedMaxNumber": "Maksimalt antall tillatte tagger er nådd", + "tagWarnDuplicate": "Duplisert tagg {tagText} ble ikke lagt til", + "supportKeyInvalid": "Ugyldig nøkkel", + "supportKeyInvalidDescription": "Din supporternøkkel er ugyldig.", + "supportKeyValid": "Gyldig nøkkel", + "supportKeyValidDescription": "Din supporternøkkel er validert. Takk for din støtte!", + "supportKeyErrorValidationDescription": "Klarte ikke å validere supporternøkkel.", + "supportKey": "Støtt utviklingen og adopter en Pangolin!", + "supportKeyDescription": "Kjøp en supporternøkkel for å hjelpe oss med å fortsette utviklingen av Pangolin for fellesskapet. Ditt bidrag lar oss bruke mer tid på å vedlikeholde og legge til nye funksjoner i applikasjonen for alle. Vi vil aldri bruke dette til å legge funksjoner bak en betalingsmur. Dette er atskilt fra enhver kommersiell utgave.", + "supportKeyPet": "Du vil også få adoptere og møte din helt egen kjæledyr-Pangolin!", + "supportKeyPurchase": "Betalinger behandles via GitHub. Etterpå kan du hente nøkkelen din på", + "supportKeyPurchaseLink": "vår nettside", + "supportKeyPurchase2": "og løse den inn her.", + "supportKeyLearnMore": "Lær mer.", + "supportKeyOptions": "Vennligst velg det alternativet som passer deg best.", + "supportKetOptionFull": "Full støttespiller", + "forWholeServer": "For hele serveren", + "lifetimePurchase": "Livstidskjøp", + "supporterStatus": "Supporterstatus", + "buy": "Kjøp", + "supportKeyOptionLimited": "Begrenset støttespiller", + "forFiveUsers": "For 5 eller færre brukere", + "supportKeyRedeem": "Løs inn supporternøkkel", + "supportKeyHideSevenDays": "Skjul i 7 dager", + "supportKeyEnter": "Skriv inn supporternøkkel", + "supportKeyEnterDescription": "Møt din helt egen kjæledyr-Pangolin!", + "githubUsername": "GitHub-brukernavn", + "supportKeyInput": "Supporternøkkel", + "supportKeyBuy": "Kjøp supporternøkkel", + "logoutError": "Feil ved utlogging", + "signingAs": "Logget inn som", + "serverAdmin": "Serveradministrator", + "otpEnable": "Aktiver tofaktor", + "otpDisable": "Deaktiver tofaktor", + "logout": "Logg ut", + "licenseTierProfessionalRequired": "Profesjonell utgave påkrevd", + "licenseTierProfessionalRequiredDescription": "Denne funksjonen er kun tilgjengelig i den profesjonelle utgaven.", + "actionGetOrg": "Hent organisasjon", + "actionUpdateOrg": "Oppdater organisasjon", + "actionUpdateUser": "Oppdater bruker", + "actionGetUser": "Hent bruker", + "actionGetOrgUser": "Hent organisasjonsbruker", + "actionListOrgDomains": "List opp organisasjonsdomener", + "actionCreateSite": "Opprett område", + "actionDeleteSite": "Slett område", + "actionGetSite": "Hent område", + "actionListSites": "List opp områder", + "actionUpdateSite": "Oppdater område", + "actionListSiteRoles": "List opp tillatte områderoller", + "actionCreateResource": "Opprett ressurs", + "actionDeleteResource": "Slett ressurs", + "actionGetResource": "Hent ressurs", + "actionListResource": "List opp ressurser", + "actionUpdateResource": "Oppdater ressurs", + "actionListResourceUsers": "List opp ressursbrukere", + "actionSetResourceUsers": "Angi ressursbrukere", + "actionSetAllowedResourceRoles": "Angi tillatte ressursroller", + "actionListAllowedResourceRoles": "List opp tillatte ressursroller", + "actionSetResourcePassword": "Angi ressurspassord", + "actionSetResourcePincode": "Angi ressurspinkode", + "actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs", + "actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs", + "actionCreateTarget": "Opprett mål", + "actionDeleteTarget": "Slett mål", + "actionGetTarget": "Hent mål", + "actionListTargets": "List opp mål", + "actionUpdateTarget": "Oppdater mål", + "actionCreateRole": "Opprett rolle", + "actionDeleteRole": "Slett rolle", + "actionGetRole": "Hent rolle", + "actionListRole": "List opp roller", + "actionUpdateRole": "Oppdater rolle", + "actionListAllowedRoleResources": "List opp tillatte rolleressurser", + "actionInviteUser": "Inviter bruker", + "actionRemoveUser": "Fjern bruker", + "actionListUsers": "List opp brukere", + "actionAddUserRole": "Legg til brukerrolle", + "actionGenerateAccessToken": "Generer tilgangstoken", + "actionDeleteAccessToken": "Slett tilgangstoken", + "actionListAccessTokens": "List opp tilgangstokener", + "actionCreateResourceRule": "Opprett ressursregel", + "actionDeleteResourceRule": "Slett ressursregel", + "actionListResourceRules": "List opp ressursregler", + "actionUpdateResourceRule": "Oppdater ressursregel", + "actionListOrgs": "List opp organisasjoner", + "actionCheckOrgId": "Sjekk ID", + "actionCreateOrg": "Opprett organisasjon", + "actionDeleteOrg": "Slett organisasjon", + "actionListApiKeys": "List opp API-nøkler", + "actionListApiKeyActions": "List opp API-nøkkelhandlinger", + "actionSetApiKeyActions": "Angi tillatte handlinger for API-nøkkel", + "actionCreateApiKey": "Opprett API-nøkkel", + "actionDeleteApiKey": "Slett API-nøkkel", + "actionCreateIdp": "Opprett IDP", + "actionUpdateIdp": "Oppdater IDP", + "actionDeleteIdp": "Slett IDP", + "actionListIdps": "List opp IDP-er", + "actionGetIdp": "Hent IDP", + "actionCreateIdpOrg": "Opprett IDP-organisasjonspolicy", + "actionDeleteIdpOrg": "Slett IDP-organisasjonspolicy", + "actionListIdpOrgs": "List opp IDP-organisasjoner", + "actionUpdateIdpOrg": "Oppdater IDP-organisasjon", + "noneSelected": "Ingen valgt", + "orgNotFound2": "Ingen organisasjoner funnet.", + "searchProgress": "Søker...", + "create": "Opprett", + "orgs": "Organisasjoner", + "loginError": "En feil oppstod under innlogging", + "passwordForgot": "Glemt passordet ditt?", + "otpAuth": "Tofaktorautentisering", + "otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.", + "otpAuthSubmit": "Send inn kode", + "idpContinue": "Eller fortsett med", + "otpAuthBack": "Tilbake til innlogging", + "navbar": "Navigasjonsmeny", + "navbarDescription": "Hovednavigasjonsmeny for applikasjonen", + "navbarDocsLink": "Dokumentasjon", + "commercialEdition": "Kommersiell utgave", + "otpErrorEnable": "Kunne ikke aktivere 2FA", + "otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA", + "otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode", + "otpSetupCheckCodeRetry": "Ugyldig kode. Vennligst prøv igjen.", + "otpSetup": "Aktiver tofaktorautentisering", + "otpSetupDescription": "Sikre kontoen din med et ekstra lag med beskyttelse", + "otpSetupScanQr": "Skann denne QR-koden med autentiseringsappen din eller skriv inn den hemmelige nøkkelen manuelt:", + "otpSetupSecretCode": "Autentiseringskode", + "otpSetupSuccess": "Tofaktorautentisering aktivert", + "otpSetupSuccessStoreBackupCodes": "Kontoen din er nå sikrere. Ikke glem å lagre reservekodene dine.", + "otpErrorDisable": "Kunne ikke deaktivere 2FA", + "otpErrorDisableDescription": "En feil oppstod under deaktivering av 2FA", + "otpRemove": "Deaktiver tofaktorautentisering", + "otpRemoveDescription": "Deaktiver tofaktorautentisering for kontoen din", + "otpRemoveSuccess": "Tofaktorautentisering deaktivert", + "otpRemoveSuccessMessage": "Tofaktorautentisering er deaktivert for kontoen din. Du kan aktivere den igjen når som helst.", + "otpRemoveSubmit": "Deaktiver 2FA", + "paginator": "Side {current} av {last}", + "paginatorToFirst": "Gå til første side", + "paginatorToPrevious": "Gå til forrige side", + "paginatorToNext": "Gå til neste side", + "paginatorToLast": "Gå til siste side", + "copyText": "Kopier tekst", + "copyTextFailed": "Klarte ikke å kopiere tekst: ", + "copyTextClipboard": "Kopier til utklippstavle", + "inviteErrorInvalidConfirmation": "Ugyldig bekreftelse", + "passwordRequired": "Passord er påkrevd", + "allowAll": "Tillat alle", + "permissionsAllowAll": "Tillat alle rettigheter", + "githubUsernameRequired": "GitHub-brukernavn er påkrevd", + "supportKeyRequired": "supporternøkkel er påkrevd", + "passwordRequirementsChars": "Passordet må være minst 8 tegn", + "language": "Språk", + "verificationCodeRequired": "Kode er påkrevd", + "userErrorNoUpdate": "Ingen bruker å oppdatere", + "siteErrorNoUpdate": "Ingen område å oppdatere", + "resourceErrorNoUpdate": "Ingen ressurs å oppdatere", + "authErrorNoUpdate": "Ingen autentiseringsinfo å oppdatere", + "orgErrorNoUpdate": "Ingen organisasjon å oppdatere", + "orgErrorNoProvided": "Ingen organisasjon angitt", + "apiKeysErrorNoUpdate": "Ingen API-nøkkel å oppdatere", + "sidebarOverview": "Oversikt", + "sidebarHome": "Hjem", + "sidebarSites": "Områder", + "sidebarResources": "Ressurser", + "sidebarAccessControl": "Tilgangskontroll", + "sidebarUsers": "Brukere", + "sidebarInvitations": "Invitasjoner", + "sidebarRoles": "Roller", + "sidebarShareableLinks": "Delbare lenker", + "sidebarApiKeys": "API-nøkler", + "sidebarSettings": "Innstillinger", + "sidebarAllUsers": "Alle brukere", + "sidebarIdentityProviders": "Identitetsleverandører", + "sidebarLicense": "Lisens", + "sidebarClients": "Klienter (Beta)", + "sidebarDomains": "Domener", + "enableDockerSocket": "Aktiver Docker Socket", + "enableDockerSocketDescription": "Aktiver Docker Socket-oppdagelse for å fylle ut containerinformasjon. Socket-stien må oppgis til Newt.", + "enableDockerSocketLink": "Lær mer", + "viewDockerContainers": "Vis Docker-containere", + "containersIn": "Containere i {siteName}", + "selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.", + "containerName": "Navn", + "containerImage": "Bilde", + "containerState": "Tilstand", + "containerNetworks": "Nettverk", + "containerHostnameIp": "Vertsnavn/IP", + "containerLabels": "Etiketter", + "containerLabelsCount": "{count, plural, en {# etikett} other {# etiketter}}", + "containerLabelsTitle": "Containeretiketter", + "containerLabelEmpty": "", + "containerPorts": "Porter", + "containerPortsMore": "+{count} til", + "containerActions": "Handlinger", + "select": "Velg", + "noContainersMatchingFilters": "Ingen containere funnet som matcher de nåværende filtrene.", + "showContainersWithoutPorts": "Vis containere uten porter", + "showStoppedContainers": "Vis stoppede containere", + "noContainersFound": "Ingen containere funnet. Sørg for at Docker-containere kjører.", + "searchContainersPlaceholder": "Søk blant {count} containere...", + "searchResultsCount": "{count, plural, en {# resultat} other {# resultater}}", + "filters": "Filtre", + "filterOptions": "Filteralternativer", + "filterPorts": "Porter", + "filterStopped": "Stoppet", + "clearAllFilters": "Fjern alle filtre", + "columns": "Kolonner", + "toggleColumns": "Vis/skjul kolonner", + "refreshContainersList": "Oppdater containerliste", + "searching": "Søker...", + "noContainersFoundMatching": "Ingen containere funnet som matcher \"{filter}\".", + "light": "lys", + "dark": "mørk", + "system": "system", + "theme": "Tema", + "subnetRequired": "Subnett er påkrevd", + "initialSetupTitle": "Førstegangsoppsett av server", + "initialSetupDescription": "Opprett den første serveradministratorkontoen. Det kan bare finnes én serveradministrator. Du kan alltid endre denne påloggingsinformasjonen senere.", + "createAdminAccount": "Opprett administratorkonto", + "setupErrorCreateAdmin": "En feil oppstod under opprettelsen av serveradministratorkontoen.", + "certificateStatus": "Sertifikatstatus", + "loading": "Laster inn", + "restart": "Start på nytt", + "domains": "Domener", + "domainsDescription": "Administrer domener for organisasjonen din", + "domainsSearch": "Søk i domener...", + "domainAdd": "Legg til domene", + "domainAddDescription": "Registrer et nytt domene hos organisasjonen din", + "domainCreate": "Opprett domene", + "domainCreatedDescription": "Domene ble opprettet", + "domainDeletedDescription": "Domene ble slettet", + "domainQuestionRemove": "Er du sikker på at du vil fjerne domenet {domain} fra kontoen din?", + "domainMessageRemove": "Når domenet er fjernet, vil det ikke lenger være knyttet til kontoen din.", + "domainMessageConfirm": "For å bekrefte, vennligst skriv inn domenenavnet nedenfor.", + "domainConfirmDelete": "Bekreft sletting av domene", + "domainDelete": "Slett domene", + "domain": "Domene", + "selectDomainTypeNsName": "Domenedelegering (NS)", + "selectDomainTypeNsDescription": "Dette domenet og alle dets underdomener. Bruk dette når du vil kontrollere en hel domenesone.", + "selectDomainTypeCnameName": "Enkelt domene (CNAME)", + "selectDomainTypeCnameDescription": "Bare dette spesifikke domenet. Bruk dette for individuelle underdomener eller spesifikke domeneoppføringer.", + "selectDomainTypeWildcardName": "Wildcard-domene", + "selectDomainTypeWildcardDescription": "Dette domenet og dets underdomener.", + "domainDelegation": "Enkelt domene", + "selectType": "Velg en type", + "actions": "Handlinger", + "refresh": "Oppdater", + "refreshError": "Klarte ikke å oppdatere data", + "verified": "Verifisert", + "pending": "Venter", + "sidebarBilling": "Fakturering", + "billing": "Fakturering", + "orgBillingDescription": "Administrer faktureringsinformasjon og abonnementer", + "github": "GitHub", + "pangolinHosted": "Driftet av Pangolin", + "fossorial": "Fossorial", + "completeAccountSetup": "Fullfør kontooppsett", + "completeAccountSetupDescription": "Angi passordet ditt for å komme i gang", + "accountSetupSent": "Vi sender en oppsettskode for kontoen til denne e-postadressen.", + "accountSetupCode": "Oppsettskode", + "accountSetupCodeDescription": "Sjekk e-posten din for oppsettskoden.", + "passwordCreate": "Opprett passord", + "passwordCreateConfirm": "Bekreft passord", + "accountSetupSubmit": "Send oppsettskode", + "completeSetup": "Fullfør oppsett", + "accountSetupSuccess": "Kontooppsett fullført! Velkommen til Pangolin!", + "documentation": "Dokumentasjon", + "saveAllSettings": "Lagre alle innstillinger", + "settingsUpdated": "Innstillinger oppdatert", + "settingsUpdatedDescription": "Alle innstillinger er oppdatert", + "settingsErrorUpdate": "Klarte ikke å oppdatere innstillinger", + "settingsErrorUpdateDescription": "En feil oppstod under oppdatering av innstillinger", + "sidebarCollapse": "Skjul", + "sidebarExpand": "Utvid", + "newtUpdateAvailable": "Oppdatering tilgjengelig", + "newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.", + "domainPickerEnterDomain": "Domene", + "domainPickerPlaceholder": "minapp.eksempel.com, api.v1.mittdomene.com, eller bare minapp", + "domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.", + "domainPickerDescriptionSaas": "Skriv inn et fullt domene, underdomene eller bare et navn for å se tilgjengelige alternativer", + "domainPickerTabAll": "Alle", + "domainPickerTabOrganization": "Organisasjon", + "domainPickerTabProvided": "Levert", + "domainPickerSortAsc": "A-Å", + "domainPickerSortDesc": "Å-A", + "domainPickerCheckingAvailability": "Sjekker tilgjengelighet...", + "domainPickerNoMatchingDomains": "Ingen samsvarende domener funnet. Prøv et annet domene eller sjekk organisasjonens domeneinnstillinger.", + "domainPickerOrganizationDomains": "Organisasjonsdomener", + "domainPickerProvidedDomains": "Leverte domener", + "domainPickerSubdomain": "Underdomene: {subdomain}", + "domainPickerNamespace": "Navnerom: {namespace}", + "domainPickerShowMore": "Vis mer", + "domainNotFound": "Domene ikke funnet", + "domainNotFoundDescription": "Denne ressursen er deaktivert fordi domenet ikke lenger eksisterer i systemet vårt. Vennligst angi et nytt domene for denne ressursen.", + "failed": "Mislyktes", + "createNewOrgDescription": "Opprett en ny organisasjon", + "organization": "Organisasjon", + "port": "Port", + "securityKeyManage": "Administrer sikkerhetsnøkler", + "securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering", + "securityKeyRegister": "Registrer ny sikkerhetsnøkkel", + "securityKeyList": "Dine sikkerhetsnøkler", + "securityKeyNone": "Ingen sikkerhetsnøkler er registrert enda", + "securityKeyNameRequired": "Navn er påkrevd", + "securityKeyRemove": "Fjern", + "securityKeyLastUsed": "Sist brukt: {date}", + "securityKeyNameLabel": "Navn på sikkerhetsnøkkel", + "securityKeyRegisterSuccess": "Sikkerhetsnøkkel registrert", + "securityKeyRegisterError": "Klarte ikke å registrere sikkerhetsnøkkel", + "securityKeyRemoveSuccess": "Sikkerhetsnøkkel fjernet", + "securityKeyRemoveError": "Klarte ikke å fjerne sikkerhetsnøkkel", + "securityKeyLoadError": "Klarte ikke å laste inn sikkerhetsnøkler", + "securityKeyLogin": "Fortsett med sikkerhetsnøkkel", + "securityKeyAuthError": "Klarte ikke å autentisere med sikkerhetsnøkkel", + "securityKeyRecommendation": "Registrer en reservesikkerhetsnøkkel på en annen enhet for å sikre at du alltid har tilgang til kontoen din.", + "registering": "Registrerer...", + "securityKeyPrompt": "Vennligst verifiser identiteten din med sikkerhetsnøkkelen. Sørg for at sikkerhetsnøkkelen er koblet til og klar.", + "securityKeyBrowserNotSupported": "Nettleseren din støtter ikke sikkerhetsnøkler. Vennligst bruk en moderne nettleser som Chrome, Firefox eller Safari.", + "securityKeyPermissionDenied": "Vennligst tillat tilgang til sikkerhetsnøkkelen din for å fortsette innloggingen.", + "securityKeyRemovedTooQuickly": "Vennligst hold sikkerhetsnøkkelen tilkoblet til innloggingsprosessen er fullført.", + "securityKeyNotSupported": "Sikkerhetsnøkkelen din er kanskje ikke kompatibel. Vennligst prøv en annen sikkerhetsnøkkel.", + "securityKeyUnknownError": "Det oppstod et problem med å bruke sikkerhetsnøkkelen din. Vennligst prøv igjen.", + "twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.", + "twoFactor": "Tofaktorautentisering", + "adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.", + "continueToApplication": "Fortsett til applikasjonen", + "securityKeyAdd": "Legg til sikkerhetsnøkkel", + "securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel", + "securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den", + "securityKeyTwoFactorRequired": "Tofaktorautentisering påkrevd", + "securityKeyTwoFactorDescription": "Vennligst skriv inn koden for tofaktorautentisering for å registrere sikkerhetsnøkkelen", + "securityKeyTwoFactorRemoveDescription": "Vennligst skriv inn koden for tofaktorautentisering for å fjerne sikkerhetsnøkkelen", + "securityKeyTwoFactorCode": "Tofaktorkode", + "securityKeyRemoveTitle": "Fjern sikkerhetsnøkkel", + "securityKeyRemoveDescription": "Skriv inn passordet ditt for å fjerne sikkerhetsnøkkelen \"{name}\"", + "securityKeyNoKeysRegistered": "Ingen sikkerhetsnøkler registrert", + "securityKeyNoKeysDescription": "Legg til en sikkerhetsnøkkel for å øke sikkerheten på kontoen din", + "createDomainRequired": "Domene er påkrevd", + "createDomainAddDnsRecords": "Legg til DNS-oppføringer", + "createDomainAddDnsRecordsDescription": "Legg til følgende DNS-oppføringer hos din domeneleverandør for å fullføre oppsettet.", + "createDomainNsRecords": "NS-oppføringer", + "createDomainRecord": "Oppføring", + "createDomainType": "Type:", + "createDomainName": "Navn:", + "createDomainValue": "Verdi:", + "createDomainCnameRecords": "CNAME-oppføringer", + "createDomainARecords": "A-oppføringer", + "createDomainRecordNumber": "Oppføring {number}", + "createDomainTxtRecords": "TXT-oppføringer", + "createDomainSaveTheseRecords": "Lagre disse oppføringene", + "createDomainSaveTheseRecordsDescription": "Sørg for å lagre disse DNS-oppføringene, da du ikke vil se dem igjen.", + "createDomainDnsPropagation": "DNS-propagering", + "createDomainDnsPropagationDescription": "DNS-endringer kan ta litt tid å propagere over internett. Dette kan ta fra noen få minutter til 48 timer, avhengig av din DNS-leverandør og TTL-innstillinger.", + "resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser", + "resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser", + "signUpTerms": { + "IAgreeToThe": "Jeg godtar", + "termsOfService": "brukervilkårene", + "and": "og", + "privacyPolicy": "personvernerklæringen" + }, + "siteRequired": "Område er påkrevd.", + "olmTunnel": "Olm-tunnel", + "olmTunnelDescription": "Bruk Olm for klienttilkobling", + "errorCreatingClient": "Feil ved oppretting av klient", + "clientDefaultsNotFound": "Klientstandarder ikke funnet", + "createClient": "Opprett klient", + "createClientDescription": "Opprett en ny klient for å koble til dine områder", + "seeAllClients": "Se alle klienter", + "clientInformation": "Klientinformasjon", + "clientNamePlaceholder": "Klientnavn", + "address": "Adresse", + "subnetPlaceholder": "Subnett", + "addressDescription": "Adressen denne klienten vil bruke for tilkobling", + "selectSites": "Velg områder", + "sitesDescription": "Klienten vil ha tilkobling til de valgte områdene", + "clientInstallOlm": "Installer Olm", + "clientInstallOlmDescription": "Få Olm til å kjøre på systemet ditt", + "clientOlmCredentials": "Olm-legitimasjon", + "clientOlmCredentialsDescription": "Slik vil Olm autentisere med serveren", + "olmEndpoint": "Olm-endepunkt", + "olmId": "Olm-ID", + "olmSecretKey": "Olm hemmelig nøkkel", + "clientCredentialsSave": "Lagre din legitimasjon", + "clientCredentialsSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", + "generalSettingsDescription": "Konfigurer de generelle innstillingene for denne klienten", + "clientUpdated": "Klient oppdatert", + "clientUpdatedDescription": "Klienten er blitt oppdatert.", + "clientUpdateFailed": "Klarte ikke å oppdatere klient", + "clientUpdateError": "En feil oppstod under oppdatering av klienten.", + "sitesFetchFailed": "Klarte ikke å hente områder", + "sitesFetchError": "En feil oppstod under henting av områder.", + "olmErrorFetchReleases": "En feil oppstod under henting av Olm-utgivelser.", + "olmErrorFetchLatest": "En feil oppstod under henting av den nyeste Olm-utgivelsen.", + "remoteSubnets": "Fjern-subnett", + "enterCidrRange": "Skriv inn CIDR-område", + "remoteSubnetsDescription": "Legg til CIDR-områder som kan få fjerntilgang til dette området. Bruk format som 10.0.0.0/24 eller 192.168.1.0/24.", + "resourceEnableProxy": "Aktiver offentlig proxy", + "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", + "externalProxyEnabled": "Ekstern proxy aktivert" +} \ No newline at end of file From 338b7a8c1308fc79fe0bca7fdc1413984984b1d4 Mon Sep 17 00:00:00 2001 From: EliasT05 <46528691+EliasTors@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:33:11 +0200 Subject: [PATCH 062/219] Added nb-NO to list of locals --- src/components/LocaleSwitcher.tsx | 4 ++++ src/i18n/config.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/LocaleSwitcher.tsx b/src/components/LocaleSwitcher.tsx index 080c8f7b..bb96df4e 100644 --- a/src/components/LocaleSwitcher.tsx +++ b/src/components/LocaleSwitcher.tsx @@ -48,6 +48,10 @@ export default function LocaleSwitcher() { { value: 'zh-CN', label: '简体中文' + }, + { + value: 'nb-NO', + label: 'Norsk (Bokmål)' } ]} /> diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 305d66d3..c6f3a85f 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -1,4 +1,4 @@ export type Locale = (typeof locales)[number]; -export const locales = ['en-US', 'es-ES', 'fr-FR', 'de-DE', 'nl-NL', 'it-IT', 'pl-PL', 'pt-PT', 'tr-TR', 'zh-CN'] as const; +export const locales = ['en-US', 'es-ES', 'fr-FR', 'de-DE', 'nl-NL', 'it-IT', 'pl-PL', 'pt-PT', 'tr-TR', 'zh-CN', 'nb-NO'] as const; export const defaultLocale: Locale = 'en-US'; \ No newline at end of file From d3d1dcfe1db4e7821f91ca4fc28090578c1aa775 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 9 Aug 2025 12:24:58 -0700 Subject: [PATCH 063/219] New translations en-us.json (Norwegian Bokmal) --- messages/nb-NO.json | 2648 ++++++++++++++++++++++--------------------- 1 file changed, 1326 insertions(+), 1322 deletions(-) diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 320418e0..92b52d01 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -1,1323 +1,1327 @@ { - "setupCreate": "Lag din organisasjon, område og dine ressurser", - "setupNewOrg": "Ny Organisasjon", - "setupCreateOrg": "Opprett organisasjon", - "setupCreateResources": "Opprett ressurser", - "setupOrgName": "Organisasjonsnavn", - "orgDisplayName": "Dette er visningsnavnet til organisasjonen din.", - "orgId": "Organisasjons-ID", - "setupIdentifierMessage": "Dette er den unike identifikator for din organisasjon. Dette er separat fra visningsnavnet.", - "setupErrorIdentifier": "Organisasjons-ID er allerede tatt. Vennligst velg en annen.", - "componentsErrorNoMemberCreate": "Du er for øyeblikket ikke medlem av noen organisasjoner. Lag en organisasjon for å komme i gang.", - "componentsErrorNoMember": "Du er for øyeblikket ikke medlem av noen organisasjoner.", - "welcome": "Velkommen!", - "welcomeTo": "Velkommen til", - "componentsCreateOrg": "Lag en Organisasjon", - "componentsMember": "Du er {count, plural, =0 {ikke medlem av noen organisasjoner} =1 {medlem av en organisasjon} other {medlem av # organisasjoner}}.", - "componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", - "dismiss": "Avvis", - "componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", - "componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!", - "inviteErrorNotValid": "Beklager, men det ser ut som invitasjonen du prøver å bruke ikke har blitt akseptert eller ikke er gyldig lenger.", - "inviteErrorUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til, ikke er for denne brukeren.", - "inviteLoginUser": "Vennligst sjekk at du er logget inn som riktig bruker.", - "inviteErrorNoUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til ikke er for en bruker som eksisterer.", - "inviteCreateUser": "Vennligst opprett en konto først.", - "goHome": "Gå hjem", - "inviteLogInOtherUser": "Logg inn som en annen bruker", - "createAnAccount": "Lag konto", - "inviteNotAccepted": "Invitasjonen ikke akseptert", - "authCreateAccount": "Opprett en konto for å komme i gang", - "authNoAccount": "Har du ikke konto?", - "email": "E-post", - "password": "Passord", - "confirmPassword": "Bekreft Passord", - "createAccount": "Opprett Konto", - "viewSettings": "Vis Innstillinger", - "delete": "Slett", - "name": "Navn", - "online": "Online", - "offline": "Frakoblet", - "site": "Område", - "dataIn": "Data Inn", - "dataOut": "Data Ut", - "connectionType": "Tilkoblingstype", - "tunnelType": "Tunneltype", - "local": "Lokal", - "edit": "Rediger", - "siteConfirmDelete": "Bekreft Sletting av Område", - "siteDelete": "Slett Område", - "siteMessageRemove": "Når området slettes, vil det ikke lenger være tilgjengelig. Alle ressurser og mål assosiert med området vil også bli slettet.", - "siteMessageConfirm": "For å bekrefte, vennligst skriv inn navnet i området nedenfor.", - "siteQuestionRemove": "Er du sikker på at du vil slette området {selectedSite} fra organisasjonen?", - "siteManageSites": "Administrer Områder", - "siteDescription": "Tillat tilkobling til nettverket ditt gjennom sikre tunneler", - "siteCreate": "Opprett område", - "siteCreateDescription2": "Følg trinnene nedenfor for å opprette og koble til et nytt område", - "siteCreateDescription": "Opprett et nytt område for å begynne å koble til ressursene dine", - "close": "Lukk", - "siteErrorCreateKeyPair": "Nøkkelpar eller standardinnstillinger for område ikke funnet", - "siteErrorCreate": "Feil ved oppretting av område", - "siteErrorCreateDefaults": "Standardinnstillinger for område ikke funnet", - "method": "Metode", - "siteMethodDescription": "Slik eksponerer du tilkoblinger.", - "siteLearnNewt": "Lær hvordan du installerer Newt på systemet ditt", - "siteSeeConfigOnce": "Du kan kun se konfigurasjonen én gang.", - "siteLoadWGConfig": "Laster WireGuard-konfigurasjon...", - "siteDocker": "Utvid for detaljer om Docker-deployment", - "toggle": "Veksle", - "dockerCompose": "Docker Compose", - "dockerRun": "Docker Run", - "siteLearnLocal": "Lokale områder tunnelerer ikke, lær mer", - "siteConfirmCopy": "Jeg har kopiert konfigurasjonen", - "searchSitesProgress": "Søker i områder...", - "siteAdd": "Legg til område", - "siteInstallNewt": "Installer Newt", - "siteInstallNewtDescription": "Få Newt til å kjøre på systemet ditt", - "WgConfiguration": "WireGuard Konfigurasjon", - "WgConfigurationDescription": "Bruk følgende konfigurasjon for å koble til nettverket ditt", - "operatingSystem": "Operativsystem", - "commands": "Kommandoer", - "recommended": "Anbefalt", - "siteNewtDescription": "For den beste brukeropplevelsen, bruk Newt. Den bruker WireGuard i bakgrunnen og lar deg adressere dine private ressurser med deres LAN-adresse på ditt private nettverk fra Pangolin-dashbordet.", - "siteRunsInDocker": "Kjører i Docker", - "siteRunsInShell": "Kjører i skall på macOS, Linux og Windows", - "siteErrorDelete": "Feil ved sletting av området", - "siteErrorUpdate": "Klarte ikke å oppdatere området", - "siteErrorUpdateDescription": "En feil oppstod under oppdatering av området.", - "siteUpdated": "Område oppdatert", - "siteUpdatedDescription": "Området har blitt oppdatert.", - "siteGeneralDescription": "Konfigurer de generelle innstillingene for dette området", - "siteSettingDescription": "Konfigurer innstillingene for området ditt", - "siteSetting": "{siteName} Innstillinger", - "siteNewtTunnel": "Newt Tunnel (Anbefalt)", - "siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.", - "siteWg": "Grunnleggende WireGuard", - "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", - "siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.", - "siteSeeAll": "Se alle områder", - "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", - "siteNewtCredentials": "Newt påloggingsinformasjon", - "siteNewtCredentialsDescription": "Slik vil Newt autentisere seg mot serveren", - "siteCredentialsSave": "Lagre påloggingsinformasjonen din", - "siteCredentialsSaveDescription": "Du vil kun kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", - "siteInfo": "Områdeinformasjon", - "status": "Status", - "shareTitle": "Administrer delingslenker", - "shareDescription": "Opprett delbare lenker for å gi midlertidig eller permanent tilgang til ressursene dine", - "shareSearch": "Søk delingslenker...", - "shareCreate": "Opprett delingslenke", - "shareErrorDelete": "Klarte ikke å slette lenke", - "shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke", - "shareDeleted": "Lenke slettet", - "shareDeletedDescription": "Lenken har blitt slettet", - "shareTokenDescription": "Din tilgangsnøkkel kan sendes på to måter: som en query parameter eller i request headers. Disse må sendes fra klienten på hver forespørsel for autentisert tilgang.", - "accessToken": "Tilgangsnøkkel", - "usageExamples": "Brukseksempler", - "tokenId": "Token-ID", - "requestHeades": "Request Headers", - "queryParameter": "Query Parameter", - "importantNote": "Viktig merknad", - "shareImportantDescription": "Av sikkerhetsgrunner anbefales det å bruke headere fremfor query parametere der det er mulig, da query parametere kan logges i serverlogger eller nettleserhistorikk.", - "token": "Token", - "shareTokenSecurety": "Hold tilgangsnøkkelen ditt sikkert. Ikke del i offentlig tilgjengelige områder eller klientkode.", - "shareErrorFetchResource": "Klarte ikke å hente ressurser", - "shareErrorFetchResourceDescription": "En feil oppstod under henting av ressursene", - "shareErrorCreate": "Mislyktes med å opprette delingslenke", - "shareErrorCreateDescription": "Det oppsto en feil ved opprettelse av delingslenken", - "shareCreateDescription": "Alle med denne lenken får tilgang til ressursen", - "shareTitleOptional": "Tittel (valgfritt)", - "expireIn": "Utløper om", - "neverExpire": "Utløper aldri", - "shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.", - "shareSeeOnce": "Du får bare se denne lenken én gang. Pass på å kopiere den.", - "shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.", - "shareTokenUsage": "Se tilgangstokenbruk", - "createLink": "Opprett lenke", - "resourcesNotFound": "Ingen ressurser funnet", - "resourceSearch": "Søk i ressurser", - "openMenu": "Åpne meny", - "resource": "Ressurs", - "title": "Tittel", - "created": "Opprettet", - "expires": "Utløper", - "never": "Aldri", - "shareErrorSelectResource": "Vennligst velg en ressurs", - "resourceTitle": "Administrer Ressurser", - "resourceDescription": "Opprett sikre proxyer til dine private applikasjoner", - "resourcesSearch": "Søk i ressurser...", - "resourceAdd": "Legg til ressurs", - "resourceErrorDelte": "Feil ved sletting av ressurs", - "authentication": "Autentisering", - "protected": "Beskyttet", - "notProtected": "Ikke beskyttet", - "resourceMessageRemove": "Når den er fjernet, vil ressursen ikke lenger være tilgjengelig. Alle mål knyttet til ressursen vil også bli fjernet.", - "resourceMessageConfirm": "For å bekrefte, skriv inn navnet på ressursen nedenfor.", - "resourceQuestionRemove": "Er du sikker på at du vil fjerne ressursen {selectedResource} fra organisasjonen?", - "resourceHTTP": "HTTPS-ressurs", - "resourceHTTPDescription": "Proxy-forespørsler til appen din over HTTPS ved bruk av et underdomene eller grunndomene.", - "resourceRaw": "Rå TCP/UDP-ressurs", - "resourceRawDescription": "Proxyer forespørsler til appen din over TCP/UDP ved å bruke et portnummer.", - "resourceCreate": "Opprett ressurs", - "resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs", - "resourceSeeAll": "Se alle ressurser", - "resourceInfo": "Ressursinformasjon", - "resourceNameDescription": "Dette er visningsnavnet for ressursen.", - "siteSelect": "Velg område", - "siteSearch": "Søk i område", - "siteNotFound": "Ingen område funnet.", - "siteSelectionDescription": "Dette området vil gi tilkobling til ressursen.", - "resourceType": "Ressurstype", - "resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din", - "resourceHTTPSSettings": "HTTPS-innstillinger", - "resourceHTTPSSettingsDescription": "Konfigurer tilgang til ressursen din over HTTPS", - "domainType": "Domenetype", - "subdomain": "Underdomene", - "baseDomain": "Grunndomene", - "subdomnainDescription": "Underdomenet der ressursen din vil være tilgjengelig.", - "resourceRawSettings": "TCP/UDP-innstillinger", - "resourceRawSettingsDescription": "Konfigurer tilgang til ressursen din over TCP/UDP", - "protocol": "Protokoll", - "protocolSelect": "Velg en protokoll", - "resourcePortNumber": "Portnummer", - "resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.", - "cancel": "Avbryt", - "resourceConfig": "Konfigurasjonsutdrag", - "resourceConfigDescription": "Kopier og lim inn disse konfigurasjonsutdragene for å sette opp din TCP/UDP-ressurs", - "resourceAddEntrypoints": "Traefik: Legg til inngangspunkter", - "resourceExposePorts": "Gerbil: Eksponer Porter i Docker Compose", - "resourceLearnRaw": "Lær hvordan å konfigurere TCP/UDP-ressurser", - "resourceBack": "Tilbake til ressurser", - "resourceGoTo": "Gå til ressurs", - "resourceDelete": "Slett ressurs", - "resourceDeleteConfirm": "Bekreft sletting av ressurs", - "visibility": "Synlighet", - "enabled": "Aktivert", - "disabled": "Deaktivert", - "general": "Generelt", - "generalSettings": "Generelle innstillinger", - "proxy": "Proxy", - "rules": "Regler", - "resourceSettingDescription": "Konfigurer innstillingene på ressursen din", - "resourceSetting": "{resourceName} Innstillinger", - "alwaysAllow": "Alltid tillat", - "alwaysDeny": "Alltid avslå", - "orgSettingsDescription": "Konfigurer organisasjonens generelle innstillinger", - "orgGeneralSettings": "Organisasjonsinnstillinger", - "orgGeneralSettingsDescription": "Administrer dine organisasjonsdetaljer og konfigurasjon", - "saveGeneralSettings": "Lagre generelle innstillinger", - "saveSettings": "Lagre innstillinger", - "orgDangerZone": "Faresone", - "orgDangerZoneDescription": "Når du sletter denne organisasjonen er det ingen vei tilbake. Vennligst vær sikker.", - "orgDelete": "Slett organisasjon", - "orgDeleteConfirm": "Bekreft Sletting av Organisasjon", - "orgMessageRemove": "Denne handlingen er irreversibel og vil slette alle tilknyttede data.", - "orgMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på organisasjonen nedenfor.", - "orgQuestionRemove": "Er du sikker på at du vil fjerne organisasjonen {selectedOrg}?", - "orgUpdated": "Organisasjon oppdatert", - "orgUpdatedDescription": "Organisasjonen har blitt oppdatert.", - "orgErrorUpdate": "Kunne ikke oppdatere organisasjonen", - "orgErrorUpdateMessage": "En feil oppsto under oppdatering av organisasjonen.", - "orgErrorFetch": "Klarte ikke å hente organisasjoner", - "orgErrorFetchMessage": "Det oppstod en feil under opplisting av organisasjonene dine", - "orgErrorDelete": "Klarte ikke å slette organisasjon", - "orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.", - "orgDeleted": "Organisasjon slettet", - "orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.", - "orgMissing": "Organisasjons-ID Mangler", - "orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.", - "accessUsersManage": "Administrer brukere", - "accessUsersDescription": "Inviter brukere og gi dem roller for å administrere tilgang til organisasjonen din", - "accessUsersSearch": "Søk etter brukere...", - "accessUserCreate": "Opprett bruker", - "accessUserRemove": "Fjern bruker", - "username": "Brukernavn", - "identityProvider": "Identitetsleverandør", - "role": "Rolle", - "nameRequired": "Navn er påkrevd", - "accessRolesManage": "Administrer Roller", - "accessRolesDescription": "Konfigurer roller for å administrere tilgang til organisasjonen din", - "accessRolesSearch": "Søk etter roller...", - "accessRolesAdd": "Legg til rolle", - "accessRoleDelete": "Slett rolle", - "description": "Beskrivelse", - "inviteTitle": "Åpne invitasjoner", - "inviteDescription": "Administrer invitasjonene dine til andre brukere", - "inviteSearch": "Søk i invitasjoner...", - "minutes": "Minutter", - "hours": "Timer", - "days": "Dager", - "weeks": "Uker", - "months": "Måneder", - "years": "År", - "day": "{count, plural, en {# dag} other {# dager}}", - "apiKeysTitle": "API-nøkkel informasjon", - "apiKeysConfirmCopy2": "Du må bekrefte at du har kopiert API-nøkkelen.", - "apiKeysErrorCreate": "Feil ved oppretting av API-nøkkel", - "apiKeysErrorSetPermission": "Feil ved innstilling av tillatelser", - "apiKeysCreate": "Generer API-nøkkel", - "apiKeysCreateDescription": "Generer en ny API-nøkkel for din organisasjon", - "apiKeysGeneralSettings": "Tillatelser", - "apiKeysGeneralSettingsDescription": "Finn ut hva denne API-nøkkelen kan gjøre", - "apiKeysList": "Din API-nøkkel", - "apiKeysSave": "Lagre API-nøkkelen din", - "apiKeysSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", - "apiKeysInfo": "Din API-nøkkel er:", - "apiKeysConfirmCopy": "Jeg har kopiert API-nøkkelen", - "generate": "Generer", - "done": "Ferdig", - "apiKeysSeeAll": "Se alle API-nøkler", - "apiKeysPermissionsErrorLoadingActions": "Feil ved innlasting av API-nøkkel handlinger", - "apiKeysPermissionsErrorUpdate": "Feil ved innstilling av tillatelser", - "apiKeysPermissionsUpdated": "Tillatelser oppdatert", - "apiKeysPermissionsUpdatedDescription": "Tillatelsene har blitt oppdatert.", - "apiKeysPermissionsGeneralSettings": "Tillatelser", - "apiKeysPermissionsGeneralSettingsDescription": "Bestem hva denne API-nøkkelen kan gjøre", - "apiKeysPermissionsSave": "Lagre tillatelser", - "apiKeysPermissionsTitle": "Tillatelser", - "apiKeys": "API-nøkler", - "searchApiKeys": "Søk API-nøkler", - "apiKeysAdd": "Generer API-nøkkel", - "apiKeysErrorDelete": "Feil under sletting av API-nøkkel", - "apiKeysErrorDeleteMessage": "Feil ved sletting av API-nøkkel", - "apiKeysQuestionRemove": "Er du sikker på at du vil fjerne API-nøkkelen {selectedApiKey} fra organisasjonen?", - "apiKeysMessageRemove": "Når den er fjernet, vil API-nøkkelen ikke lenger kunne brukes.", - "apiKeysMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på API-nøkkelen nedenfor.", - "apiKeysDeleteConfirm": "Bekreft sletting av API-nøkkel", - "apiKeysDelete": "Slett API-nøkkel", - "apiKeysManage": "Administrer API-nøkler", - "apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API", - "apiKeysSettings": "{apiKeyName} Innstillinger", - "userTitle": "Administrer alle brukere", - "userDescription": "Vis og administrer alle brukere i systemet", - "userAbount": "Om brukeradministrasjon", - "userAbountDescription": "Denne tabellen viser alle rotbrukerobjekter i systemet. Hver bruker kan tilhøre flere organisasjoner. Å fjerne en bruker fra en organisasjon sletter ikke deres rotbrukerobjekt – de vil forbli i systemet. For å fullstendig fjerne en bruker fra systemet, må du slette deres rotbrukerobjekt ved å bruke slett-handlingen i denne tabellen.", - "userServer": "Serverbrukere", - "userSearch": "Søk serverbrukere...", - "userErrorDelete": "Feil ved sletting av bruker", - "userDeleteConfirm": "Bekreft sletting av bruker", - "userDeleteServer": "Slett bruker fra server", - "userMessageRemove": "Brukeren vil bli fjernet fra alle organisasjoner og vil bli fullstendig fjernet fra serveren.", - "userMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", - "userQuestionRemove": "Er du sikker på at du vil slette {selectedUser} permanent fra serveren?", - "licenseKey": "Lisensnøkkel", - "valid": "Gyldig", - "numberOfSites": "Antall områder", - "licenseKeySearch": "Søk lisensnøkler...", - "licenseKeyAdd": "Legg til lisensnøkkel", - "type": "Type", - "licenseKeyRequired": "Lisensnøkkel er påkrevd", - "licenseTermsAgree": "Du må godta lisensvilkårene", - "licenseErrorKeyLoad": "Feil ved lasting av lisensnøkler", - "licenseErrorKeyLoadDescription": "Det oppstod en feil ved lasting av lisensnøkler.", - "licenseErrorKeyDelete": "Kunne ikke slette lisensnøkkel", - "licenseErrorKeyDeleteDescription": "Det oppstod en feil ved sletting av lisensnøkkel.", - "licenseKeyDeleted": "Lisensnøkkel slettet", - "licenseKeyDeletedDescription": "Lisensnøkkelen har blitt slettet.", - "licenseErrorKeyActivate": "Aktivering av lisensnøkkel feilet", - "licenseErrorKeyActivateDescription": "Det oppstod en feil under aktivering av lisensnøkkelen.", - "licenseAbout": "Om Lisensiering", - "communityEdition": "Fellesskapsutgave", - "licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.", - "licenseKeyActivated": "Lisensnøkkel aktivert", - "licenseKeyActivatedDescription": "Lisensnøkkelen har blitt vellykket aktivert.", - "licenseErrorKeyRecheck": "En feil oppsto under verifisering av lisensnøkler", - "licenseErrorKeyRecheckDescription": "Det oppstod en feil under verifisering av lisensnøkler.", - "licenseErrorKeyRechecked": "Lisensnøkler verifisert", - "licenseErrorKeyRecheckedDescription": "Alle lisensnøkler er verifisert", - "licenseActivateKey": "Aktiver lisensnøkkel", - "licenseActivateKeyDescription": "Skriv inn en lisensnøkkel for å aktivere den.", - "licenseActivate": "Aktiver lisens", - "licenseAgreement": "Ved å krysse av denne boksen bekrefter du at du har lest og godtar lisensvilkårene som tilsvarer nivået tilknyttet lisensnøkkelen din.", - "fossorialLicense": "Vis Fossorial kommersiell lisens og abonnementsvilkår", - "licenseMessageRemove": "Dette vil fjerne lisensnøkkelen og alle tilknyttede tillatelser gitt av den.", - "licenseMessageConfirm": "For å bekrefte, vennligst skriv inn lisensnøkkelen nedenfor.", - "licenseQuestionRemove": "Er du sikker på at du vil slette lisensnøkkelen {selectedKey} ?", - "licenseKeyDelete": "Slett Lisensnøkkel", - "licenseKeyDeleteConfirm": "Bekreft sletting av lisensnøkkel", - "licenseTitle": "Behandle lisensstatus", - "licenseTitleDescription": "Se og administrer lisensnøkler i systemet", - "licenseHost": "Vertslisens", - "licenseHostDescription": "Behandle hovedlisensnøkkelen for verten.", - "licensedNot": "Ikke lisensiert", - "hostId": "Verts-ID", - "licenseReckeckAll": "Verifiser alle nøkler", - "licenseSiteUsage": "Område Bruk", - "licenseSiteUsageDecsription": "Vis antall områder som bruker denne lisensen.", - "licenseNoSiteLimit": "Det er ingen grense på antall områder som bruker en ulisensiert vert.", - "licensePurchase": "Kjøp lisens", - "licensePurchaseSites": "Kjøp flere områder", - "licenseSitesUsedMax": "{usedSites} av {maxSites} områder brukt", - "licenseSitesUsed": "{count, plural, =0 {# områder} en {# område} other {# områder}} i systemet.", - "licensePurchaseDescription": "Velg hvor mange områder du vil {selectedMode, select, license {kjøpe en lisens for. Du kan alltid legge til flere områder senere.} other {legge til din eksisterende lisens.}}", - "licenseFee": "Lisensavgift", - "licensePriceSite": "Pris per område", - "total": "Totalt", - "licenseContinuePayment": "Fortsett til betaling", - "pricingPage": "Pris oversikt", - "pricingPortal": "Se Kjøpsportal", - "licensePricingPage": "For de mest oppdaterte prisene og rabattene, vennligst besøk", - "invite": "Invitasjoner", - "inviteRegenerate": "Regenerer invitasjonen", - "inviteRegenerateDescription": "Tilbakekall tidligere invitasjon og opprette en ny", - "inviteRemove": "Fjern invitasjon", - "inviteRemoveError": "Mislyktes å fjerne invitasjon", - "inviteRemoveErrorDescription": "Det oppstod en feil under fjerning av invitasjonen.", - "inviteRemoved": "Invitasjon fjernet", - "inviteRemovedDescription": "Invitasjonen for {email} er fjernet.", - "inviteQuestionRemove": "Er du sikker på at du vil fjerne invitasjonen {email}?", - "inviteMessageRemove": "Når fjernet, vil denne invitasjonen ikke lenger være gyldig. Du kan alltid invitere brukeren på nytt senere.", - "inviteMessageConfirm": "For å bekrefte, vennligst tast inn invitasjonens e-postadresse nedenfor.", - "inviteQuestionRegenerate": "Er du sikker på at du vil generere invitasjonen på nytt for {email}? Dette vil ugyldiggjøre den forrige invitasjonen.", - "inviteRemoveConfirm": "Bekreft fjerning av invitasjon", - "inviteRegenerated": "Invitasjon fornyet", - "inviteSent": "En ny invitasjon er sendt til {email}.", - "inviteSentEmail": "Send e-postvarsel til brukeren", - "inviteGenerate": "En ny invitasjon er generert for {email}.", - "inviteDuplicateError": "Dupliser invitasjon", - "inviteDuplicateErrorDescription": "En invitasjon for denne brukeren eksisterer allerede.", - "inviteRateLimitError": "Forespørselsgrense overskredet", - "inviteRateLimitErrorDescription": "Du har overskredet grensen på 3 regenerasjoner per time. Prøv igjen senere.", - "inviteRegenerateError": "Kunne ikke regenerere invitasjon", - "inviteRegenerateErrorDescription": "Det oppsto en feil under regenerering av invitasjonen.", - "inviteValidityPeriod": "Gyldighetsperiode", - "inviteValidityPeriodSelect": "Velg gyldighetsperiode", - "inviteRegenerateMessage": "Invitasjonen er generert på nytt. Brukeren må gå til lenken nedenfor for å akseptere invitasjonen.", - "inviteRegenerateButton": "Regenerer", - "expiresAt": "Utløpstidspunkt", - "accessRoleUnknown": "Ukjent rolle", - "placeholder": "Plassholder", - "userErrorOrgRemove": "En feil oppsto under fjerning av bruker", - "userErrorOrgRemoveDescription": "Det oppstod en feil under fjerning av brukeren.", - "userOrgRemoved": "Bruker fjernet", - "userOrgRemovedDescription": "Brukeren {email} er fjernet fra organisasjonen.", - "userQuestionOrgRemove": "Er du sikker på at du vil fjerne {email} fra organisasjonen?", - "userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.", - "userMessageOrgConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", - "userRemoveOrgConfirm": "Bekreft fjerning av bruker", - "userRemoveOrg": "Fjern bruker fra organisasjon", - "users": "Brukere", - "accessRoleMember": "Medlem", - "accessRoleOwner": "Eier", - "userConfirmed": "Bekreftet", - "idpNameInternal": "Intern", - "emailInvalid": "Ugyldig e-postadresse", - "inviteValidityDuration": "Vennligst velg en varighet", - "accessRoleSelectPlease": "Vennligst velg en rolle", - "usernameRequired": "Brukernavn er påkrevd", - "idpSelectPlease": "Vennligst velg en identitetsleverandør", - "idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.", - "accessRoleErrorFetch": "En feil oppsto under henting av roller", - "accessRoleErrorFetchDescription": "En feil oppsto under henting av rollene", - "idpErrorFetch": "En feil oppsto under henting av identitetsleverandører", - "idpErrorFetchDescription": "En feil oppsto ved henting av identitetsleverandører", - "userErrorExists": "Bruker eksisterer allerede", - "userErrorExistsDescription": "Denne brukeren er allerede medlem av organisasjonen.", - "inviteError": "Kunne ikke invitere bruker", - "inviteErrorDescription": "En feil oppsto under invitering av brukeren", - "userInvited": "Bruker invitert", - "userInvitedDescription": "Brukeren er vellykket invitert.", - "userErrorCreate": "Kunne ikke opprette bruker", - "userErrorCreateDescription": "Det oppsto en feil under oppretting av brukeren", - "userCreated": "Bruker opprettet", - "userCreatedDescription": "Brukeren har blitt vellykket opprettet.", - "userTypeInternal": "Intern bruker", - "userTypeInternalDescription": "Inviter en bruker til å bli med i organisasjonen din direkte.", - "userTypeExternal": "Ekstern bruker", - "userTypeExternalDescription": "Opprett en bruker med en ekstern identitetsleverandør.", - "accessUserCreateDescription": "Følg stegene under for å opprette en ny bruker", - "userSeeAll": "Se alle brukere", - "userTypeTitle": "Brukertype", - "userTypeDescription": "Bestem hvordan du vil opprette brukeren", - "userSettings": "Brukerinformasjon", - "userSettingsDescription": "Skriv inn detaljene for den nye brukeren", - "inviteEmailSent": "Send invitasjonsepost til bruker", - "inviteValid": "Gyldig for", - "selectDuration": "Velg varighet", - "accessRoleSelect": "Velg rolle", - "inviteEmailSentDescription": "En e-post er sendt til brukeren med tilgangslenken nedenfor. De må åpne lenken for å akseptere invitasjonen.", - "inviteSentDescription": "Brukeren har blitt invitert. De må åpne lenken nedenfor for å godta invitasjonen.", - "inviteExpiresIn": "Invitasjonen utløper om {days, plural, en {# dag} other {# dager}}.", - "idpTitle": "Identitetsleverandør", - "idpSelect": "Velg identitetsleverandøren for den eksterne brukeren", - "idpNotConfigured": "Ingen identitetsleverandører er konfigurert. Vennligst konfigurer en identitetsleverandør før du oppretter eksterne brukere.", - "usernameUniq": "Dette må matche det unike brukernavnet som finnes i den valgte identitetsleverandøren.", - "emailOptional": "E-post (Valgfritt)", - "nameOptional": "Navn (valgfritt)", - "accessControls": "Tilgangskontroller", - "userDescription2": "Administrer innstillingene for denne brukeren", - "accessRoleErrorAdd": "Kunne ikke legge til bruker i rolle", - "accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.", - "userSaved": "Bruker lagret", - "userSavedDescription": "Brukeren har blitt oppdatert.", - "accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen", - "accessControlsSubmit": "Lagre tilgangskontroller", - "roles": "Roller", - "accessUsersRoles": "Administrer brukere og roller", - "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen din.", - "key": "Nøkkel", - "createdAt": "Opprettet", - "proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.", - "proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.", - "proxyEnableSSL": "Aktiver SSL (https)", - "targetErrorFetch": "Kunne ikke hente mål", - "targetErrorFetchDescription": "Det oppsto en feil under henting av mål", - "siteErrorFetch": "Klarte ikke å hente ressurs", - "siteErrorFetchDescription": "Det oppstod en feil under henting av ressurs", - "targetErrorDuplicate": "Dupliser mål", - "targetErrorDuplicateDescription": "Et mål med disse innstillingene finnes allerede", - "targetWireGuardErrorInvalidIp": "Ugyldig mål-IP", - "targetWireGuardErrorInvalidIpDescription": "Mål-IP må være i områdets undernett.", - "targetsUpdated": "Mål oppdatert", - "targetsUpdatedDescription": "Mål og innstillinger oppdatert vellykket", - "targetsErrorUpdate": "Feilet å oppdatere mål", - "targetsErrorUpdateDescription": "En feil oppsto under oppdatering av mål", - "targetTlsUpdate": "TLS-innstillinger oppdatert", - "targetTlsUpdateDescription": "Dine TLS-innstillinger er oppdatert", - "targetErrorTlsUpdate": "Feilet under oppdatering av TLS-innstillinger", - "targetErrorTlsUpdateDescription": "Det oppstod en feil under oppdatering av TLS-innstillinger", - "proxyUpdated": "Proxy-innstillinger oppdatert", - "proxyUpdatedDescription": "Proxy-innstillingene dine er oppdatert", - "proxyErrorUpdate": "En feil oppsto under oppdatering av proxyinnstillinger", - "proxyErrorUpdateDescription": "En feil oppsto under oppdatering av proxyinnstillinger", - "targetAddr": "IP / vertsnavn", - "targetPort": "Port", - "targetProtocol": "Protokoll", - "targetTlsSettings": "Sikker tilkoblings-konfigurasjon", - "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din", - "targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger", - "targetTlsSni": "TLS Servernavn (SNI)", - "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", - "targetTlsSubmit": "Lagre innstillinger", - "targets": "Målkonfigurasjon", - "targetsDescription": "Sett opp mål for å rute trafikk til tjenestene dine", - "targetStickySessions": "Aktiver klebrige sesjoner", - "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", - "methodSelect": "Velg metode", - "targetSubmit": "Legg til mål", - "targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.", - "targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.", - "targetsSubmit": "Lagre mål", - "proxyAdditional": "Ytterligere Proxy-innstillinger", - "proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger", - "proxyCustomHeader": "Tilpasset verts-header", - "proxyCustomHeaderDescription": "Verts-header som skal settes ved videresending av forespørsler. La stå tom for å bruke standardinnstillingen.", - "proxyAdditionalSubmit": "Lagre proxy-innstillinger", - "subnetMaskErrorInvalid": "Ugyldig subnettmaske. Må være mellom 0 og 32.", - "ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat", - "ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet", - "path": "Sti", - "ipAddressRange": "IP-område", - "rulesErrorFetch": "Klarte ikke å hente regler", - "rulesErrorFetchDescription": "Det oppsto en feil under henting av regler", - "rulesErrorDuplicate": "Duplisert regel", - "rulesErrorDuplicateDescription": "En regel med disse innstillingene finnes allerede", - "rulesErrorInvalidIpAddressRange": "Ugyldig CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Vennligst skriv inn en gyldig CIDR-verdi", - "rulesErrorInvalidUrl": "Ugyldig URL-sti", - "rulesErrorInvalidUrlDescription": "Skriv inn en gyldig verdi for URL-sti", - "rulesErrorInvalidIpAddress": "Ugyldig IP", - "rulesErrorInvalidIpAddressDescription": "Skriv inn en gyldig IP-adresse", - "rulesErrorUpdate": "Kunne ikke oppdatere regler", - "rulesErrorUpdateDescription": "Det oppsto en feil under oppdatering av regler", - "rulesUpdated": "Aktiver Regler", - "rulesUpdatedDescription": "Regelevalueringen har blitt oppdatert", - "rulesMatchIpAddressRangeDescription": "Angi en adresse i CIDR-format (f.eks., 103.21.244.0/22)", - "rulesMatchIpAddress": "Angi en IP-adresse (f.eks. 103.21.244.12)", - "rulesMatchUrl": "Skriv inn en URL-sti eller et mønster (f.eks. /api/v1/todos eller /api/v1/*)", - "rulesErrorInvalidPriority": "Ugyldig prioritet", - "rulesErrorInvalidPriorityDescription": "Vennligst skriv inn en gyldig prioritet", - "rulesErrorDuplicatePriority": "Dupliserte prioriteringer", - "rulesErrorDuplicatePriorityDescription": "Vennligst angi unike prioriteringer", - "ruleUpdated": "Regler oppdatert", - "ruleUpdatedDescription": "Reglene er oppdatert", - "ruleErrorUpdate": "Operasjon mislyktes", - "ruleErrorUpdateDescription": "En feil oppsto under lagringsoperasjonen", - "rulesPriority": "Prioritet", - "rulesAction": "Handling", - "rulesMatchType": "Trefftype", - "value": "Verdi", - "rulesAbout": "Om regler", - "rulesAboutDescription": "Regler lar deg kontrollere tilgang til din ressurs basert på et sett med kriterier. Du kan opprette regler for å tillate eller nekte tilgang basert på IP-adresse eller URL-sti.", - "rulesActions": "Handlinger", - "rulesActionAlwaysAllow": "Alltid Tillat: Omgå alle autentiserings metoder", - "rulesActionAlwaysDeny": "Alltid Nekt: Blokker alle forespørsler; ingen autentisering kan forsøkes", - "rulesMatchCriteria": "Samsvarende kriterier", - "rulesMatchCriteriaIpAddress": "Samsvar med en spesifikk IP-adresse", - "rulesMatchCriteriaIpAddressRange": "Samsvar et IP-adresseområde i CIDR-notasjon", - "rulesMatchCriteriaUrl": "Match en URL-sti eller et mønster", - "rulesEnable": "Aktiver regler", - "rulesEnableDescription": "Aktiver eller deaktiver regelvurdering for denne ressursen", - "rulesResource": "Konfigurasjon av ressursregler", - "rulesResourceDescription": "Konfigurere regler for tilgangskontroll til ressursen din", - "ruleSubmit": "Legg til regel", - "rulesNoOne": "Ingen regler. Legg til en regel ved å bruke skjemaet.", - "rulesOrder": "Regler evalueres etter prioritet i stigende rekkefølge.", - "rulesSubmit": "Lagre regler", - "resourceErrorCreate": "Feil under oppretting av ressurs", - "resourceErrorCreateDescription": "Det oppstod en feil under oppretting av ressursen", - "resourceErrorCreateMessage": "Feil ved oppretting av ressurs:", - "resourceErrorCreateMessageDescription": "En uventet feil oppstod", - "sitesErrorFetch": "Feil ved henting av områder", - "sitesErrorFetchDescription": "En feil oppstod ved henting av områdene", - "domainsErrorFetch": "Kunne ikke hente domener", - "domainsErrorFetchDescription": "Det oppsto en feil under henting av domenene", - "none": "Ingen", - "unknown": "Ukjent", - "resources": "Ressurser", - "resourcesDescription": "Ressurser er proxyer for applikasjoner som kjører på ditt private nettverk. Opprett en ressurs for enhver HTTP/HTTPS- eller rå TCP/UDP-tjeneste på ditt private nettverk. Hver ressurs må kobles til et område for å muliggjøre privat, sikker tilkobling gjennom en kryptert WireGuard-tunnel.", - "resourcesWireGuardConnect": "Sikker tilkobling med WireGuard-kryptering", - "resourcesMultipleAuthenticationMethods": "Konfigurer flere autentiseringsmetoder", - "resourcesUsersRolesAccess": "Bruker- og rollebasert tilgangskontroll", - "resourcesErrorUpdate": "Feilet å slå av/på ressurs", - "resourcesErrorUpdateDescription": "En feil oppstod under oppdatering av ressursen", - "access": "Tilgang", - "shareLink": "{resource} Del Lenke", - "resourceSelect": "Velg ressurs", - "shareLinks": "Del lenker", - "share": "Delbare lenker", - "shareDescription2": "Opprett delbare lenker til ressursene dine. Lenker gir midlertidig eller ubegrenset tilgang til ressursen din. Du kan konfigurere utløpsvarigheten for lenken når du oppretter den.", - "shareEasyCreate": "Enkelt å lage og dele", - "shareConfigurableExpirationDuration": "Konfigurerbar utløpsvarighet", - "shareSecureAndRevocable": "Sikker og tilbakekallbar", - "nameMin": "Navn må være minst {len} tegn.", - "nameMax": "Navn kan ikke være lengre enn {len} tegn.", - "sitesConfirmCopy": "Vennligst bekreft at du har kopiert konfigurasjonen.", - "unknownCommand": "Ukjent kommando", - "newtErrorFetchReleases": "Feilet å hente utgivelsesinfo: {err}", - "newtErrorFetchLatest": "Feil ved henting av siste utgivelse: {err}", - "newtEndpoint": "Newt endepunkt", - "newtId": "Newt-ID", - "newtSecretKey": "Newt hemmelig nøkkel", - "architecture": "Arkitektur", - "sites": "Områder", - "siteWgAnyClients": "Bruk en hvilken som helst WireGuard-klient for å koble til. Du må adressere dine interne ressurser ved å bruke peer-IP-en.", - "siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter", - "siteWgManualConfigurationRequired": "Manuell konfigurasjon påkrevd", - "userErrorNotAdminOrOwner": "Bruker er ikke administrator eller eier", - "pangolinSettings": "Innstillinger - Pangolin", - "accessRoleYour": "Din rolle:", - "accessRoleSelect2": "Velg en rolle", - "accessUserSelect": "Velg en bruker", - "otpEmailEnter": "Skriv inn én e-post", - "otpEmailEnterDescription": "Trykk enter for å legge til en e-post etter å ha tastet den inn i tekstfeltet.", - "otpEmailErrorInvalid": "Ugyldig e-postadresse. Jokertegnet (*) må være hele lokaldelen.", - "otpEmailSmtpRequired": "SMTP påkrevd", - "otpEmailSmtpRequiredDescription": "SMTP må være aktivert på serveren for å bruke engangspassord-autentisering.", - "otpEmailTitle": "Engangspassord", - "otpEmailTitleDescription": "Krev e-postbasert autentisering for ressurstilgang", - "otpEmailWhitelist": "E-post-hviteliste", - "otpEmailWhitelistList": "Hvitlistede e-poster", - "otpEmailWhitelistListDescription": "Kun brukere med disse e-postadressene vil ha tilgang til denne ressursen. De vil bli bedt om å skrive inn et engangspassord sendt til e-posten deres. Jokertegn (*@example.com) kan brukes for å tillate enhver e-postadresse fra et domene.", - "otpEmailWhitelistSave": "Lagre hvitliste", - "passwordAdd": "Legg til passord", - "passwordRemove": "Fjern passord", - "pincodeAdd": "Legg til PIN-kode", - "pincodeRemove": "Fjern PIN-kode", - "resourceAuthMethods": "Autentiseringsmetoder", - "resourceAuthMethodsDescriptions": "Tillat tilgang til ressursen via ytterligere autentiseringsmetoder", - "resourceAuthSettingsSave": "Lagret vellykket", - "resourceAuthSettingsSaveDescription": "Autentiseringsinnstillinger er lagret", - "resourceErrorAuthFetch": "Kunne ikke hente data", - "resourceErrorAuthFetchDescription": "Det oppstod en feil ved henting av data", - "resourceErrorPasswordRemove": "Feil ved fjerning av passord for ressurs", - "resourceErrorPasswordRemoveDescription": "Det oppstod en feil ved fjerning av ressurspassordet", - "resourceErrorPasswordSetup": "Feil ved innstilling av ressurspassord", - "resourceErrorPasswordSetupDescription": "Det oppstod en feil ved innstilling av ressurspassordet", - "resourceErrorPincodeRemove": "Feil ved fjerning av ressurs-PIN-koden", - "resourceErrorPincodeRemoveDescription": "Det oppstod en feil under fjerning av ressurs-pinkoden", - "resourceErrorPincodeSetup": "Feil ved innstilling av ressurs-PIN-kode", - "resourceErrorPincodeSetupDescription": "Det oppstod en feil under innstilling av ressursens PIN-kode", - "resourceErrorUsersRolesSave": "Klarte ikke å sette roller", - "resourceErrorUsersRolesSaveDescription": "En feil oppstod ved innstilling av rollene", - "resourceErrorWhitelistSave": "Feilet å lagre hvitliste", - "resourceErrorWhitelistSaveDescription": "Det oppstod en feil under lagring av hvitlisten", - "resourcePasswordSubmit": "Aktiver passordbeskyttelse", - "resourcePasswordProtection": "Passordbeskyttelse {status}", - "resourcePasswordRemove": "Ressurspassord fjernet", - "resourcePasswordRemoveDescription": "Fjerning av ressurspassordet var vellykket", - "resourcePasswordSetup": "Ressurspassord satt", - "resourcePasswordSetupDescription": "Ressurspassordet har blitt vellykket satt", - "resourcePasswordSetupTitle": "Angi passord", - "resourcePasswordSetupTitleDescription": "Sett et passord for å beskytte denne ressursen", - "resourcePincode": "PIN-kode", - "resourcePincodeSubmit": "Aktiver PIN-kodebeskyttelse", - "resourcePincodeProtection": "PIN-kodebeskyttelse {status}", - "resourcePincodeRemove": "Ressurs PIN-kode fjernet", - "resourcePincodeRemoveDescription": "Ressurspassordet ble fjernet", - "resourcePincodeSetup": "Ressurs PIN-kode satt", - "resourcePincodeSetupDescription": "Ressurs PIN-kode er satt vellykket", - "resourcePincodeSetupTitle": "Angi PIN-kode", - "resourcePincodeSetupTitleDescription": "Sett en pinkode for å beskytte denne ressursen", - "resourceRoleDescription": "Administratorer har alltid tilgang til denne ressursen.", - "resourceUsersRoles": "Brukere og Roller", - "resourceUsersRolesDescription": "Konfigurer hvilke brukere og roller som har tilgang til denne ressursen", - "resourceUsersRolesSubmit": "Lagre brukere og roller", - "resourceWhitelistSave": "Lagring vellykket", - "resourceWhitelistSaveDescription": "Hvitlisteinnstillinger er lagret", - "ssoUse": "Bruk plattform SSO", - "ssoUseDescription": "Eksisterende brukere trenger kun å logge på én gang for alle ressurser som har dette aktivert.", - "proxyErrorInvalidPort": "Ugyldig portnummer", - "subdomainErrorInvalid": "Ugyldig underdomene", - "domainErrorFetch": "Feil ved henting av domener", - "domainErrorFetchDescription": "Det oppstod en feil ved henting av domenene", - "resourceErrorUpdate": "Mislyktes å oppdatere ressurs", - "resourceErrorUpdateDescription": "Det oppstod en feil under oppdatering av ressursen", - "resourceUpdated": "Ressurs oppdatert", - "resourceUpdatedDescription": "Ressursen er oppdatert vellykket", - "resourceErrorTransfer": "Klarte ikke å overføre ressurs", - "resourceErrorTransferDescription": "En feil oppsto under overføring av ressursen", - "resourceTransferred": "Ressurs overført", - "resourceTransferredDescription": "Ressursen er overført vellykket.", - "resourceErrorToggle": "Feilet å veksle ressurs", - "resourceErrorToggleDescription": "Det oppstod en feil ved oppdatering av ressursen", - "resourceVisibilityTitle": "Synlighet", - "resourceVisibilityTitleDescription": "Fullstendig aktiver eller deaktiver ressursynlighet", - "resourceGeneral": "Generelle innstillinger", - "resourceGeneralDescription": "Konfigurer de generelle innstillingene for denne ressursen", - "resourceEnable": "Aktiver ressurs", - "resourceTransfer": "Overfør Ressurs", - "resourceTransferDescription": "Overfør denne ressursen til et annet område", - "resourceTransferSubmit": "Overfør ressurs", - "siteDestination": "Destinasjonsområde", - "searchSites": "Søk områder", - "accessRoleCreate": "Opprett rolle", - "accessRoleCreateDescription": "Opprett en ny rolle for å gruppere brukere og administrere deres tillatelser.", - "accessRoleCreateSubmit": "Opprett rolle", - "accessRoleCreated": "Rolle opprettet", - "accessRoleCreatedDescription": "Rollen er vellykket opprettet.", - "accessRoleErrorCreate": "Klarte ikke å opprette rolle", - "accessRoleErrorCreateDescription": "Det oppstod en feil under opprettelse av rollen.", - "accessRoleErrorNewRequired": "Ny rolle kreves", - "accessRoleErrorRemove": "Kunne ikke fjerne rolle", - "accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.", - "accessRoleName": "Rollenavn", - "accessRoleQuestionRemove": "Du er i ferd med å slette rollen {name}. Du kan ikke angre denne handlingen.", - "accessRoleRemove": "Fjern Rolle", - "accessRoleRemoveDescription": "Fjern en rolle fra organisasjonen", - "accessRoleRemoveSubmit": "Fjern Rolle", - "accessRoleRemoved": "Rolle fjernet", - "accessRoleRemovedDescription": "Rollen er vellykket fjernet.", - "accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.", - "manage": "Administrer", - "sitesNotFound": "Ingen områder funnet.", - "pangolinServerAdmin": "Server Admin - Pangolin", - "licenseTierProfessional": "Profesjonell lisens", - "licenseTierEnterprise": "Bedriftslisens", - "licenseTierCommercial": "Kommersiell lisens", - "licensed": "Lisensiert", - "yes": "Ja", - "no": "Nei", - "sitesAdditional": "Ytterligere områder", - "licenseKeys": "Lisensnøkler", - "sitestCountDecrease": "Reduser antall områder", - "sitestCountIncrease": "Øk antall områder", - "idpManage": "Administrer Identitetsleverandører", - "idpManageDescription": "Vis og administrer identitetsleverandører i systemet", - "idpDeletedDescription": "Identitetsleverandør slettet vellykket", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren {name} permanent?", - "idpMessageRemove": "Dette vil fjerne identitetsleverandøren og alle tilhørende konfigurasjoner. Brukere som autentiserer seg via denne leverandøren vil ikke lenger kunne logge inn.", - "idpMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på identitetsleverandøren nedenfor.", - "idpConfirmDelete": "Bekreft Sletting av Identitetsleverandør", - "idpDelete": "Slett identitetsleverandør", - "idp": "Identitetsleverandører", - "idpSearch": "Søk identitetsleverandører...", - "idpAdd": "Legg til Identitetsleverandør", - "idpClientIdRequired": "Klient-ID er påkrevd.", - "idpClientSecretRequired": "Klienthemmelighet er påkrevd.", - "idpErrorAuthUrlInvalid": "Autentiserings-URL må være en gyldig URL.", - "idpErrorTokenUrlInvalid": "Token-URL må være en gyldig URL.", - "idpPathRequired": "Identifikatorbane er påkrevd.", - "idpScopeRequired": "Omfang kreves.", - "idpOidcDescription": "Konfigurer en OpenID Connect identitetsleverandør", - "idpCreatedDescription": "Identitetsleverandør opprettet vellykket.", - "idpCreate": "Opprett identitetsleverandør", - "idpCreateDescription": "Konfigurer en ny identitetsleverandør for brukerautentisering", - "idpSeeAll": "Se alle identitetsleverandører", - "idpSettingsDescription": "Konfigurer grunnleggende informasjon for din identitetsleverandør", - "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", - "idpAutoProvisionUsers": "Automatisk brukerklargjøring", - "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", - "licenseBadge": "Profesjonell", - "idpType": "Leverandørtype", - "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", - "idpOidcConfigure": "OAuth2/OIDC-konfigurasjon", - "idpOidcConfigureDescription": "Konfigurer OAuth2/OIDC-leverandørens endepunkter og legitimasjon", - "idpClientId": "Klient-ID", - "idpClientIdDescription": "OAuth2-klient-ID-en fra identitetsleverandøren din", - "idpClientSecret": "Klienthemmelighet", - "idpClientSecretDescription": "OAuth2-klienthemmeligheten fra din identitetsleverandør", - "idpAuthUrl": "Autorisasjons-URL", - "idpAuthUrlDescription": "OAuth2 autorisasjonsendepunkt URL", - "idpTokenUrl": "Token-URL", - "idpTokenUrlDescription": "OAuth2-tokenendepunkt-URL", - "idpOidcConfigureAlert": "Viktig informasjon", - "idpOidcConfigureAlertDescription": "Etter at du har opprettet identitetsleverandøren, må du konfigurere callback-URL-en i identitetsleverandørens innstillinger. Callback-URL-en blir oppgitt etter vellykket opprettelse.", - "idpToken": "Token-konfigurasjon", - "idpTokenDescription": "Konfigurer hvordan brukerinformasjon trekkes ut fra ID-tokenet", - "idpJmespathAbout": "Om JMESPath", - "idpJmespathAboutDescription": "Stiene nedenfor bruker JMESPath-syntaks for å hente ut verdier fra ID-tokenet.", - "idpJmespathAboutDescriptionLink": "Lær mer om JMESPath", - "idpJmespathLabel": "Identifikatorsti", - "idpJmespathLabelDescription": "Stien til brukeridentifikatoren i ID-tokenet", - "idpJmespathEmailPathOptional": "E-poststi (Valgfritt)", - "idpJmespathEmailPathOptionalDescription": "Stien til brukerens e-postadresse i ID-tokenet", - "idpJmespathNamePathOptional": "Navn Sti (Valgfritt)", - "idpJmespathNamePathOptionalDescription": "Stien til brukerens navn i ID-tokenet", - "idpOidcConfigureScopes": "Omfang", - "idpOidcConfigureScopesDescription": "Mellomromseparert liste over OAuth2-omfang å be om", - "idpSubmit": "Opprett identitetsleverandør", - "orgPolicies": "Organisasjonsretningslinjer", - "idpSettings": "{idpName} Innstillinger", - "idpCreateSettingsDescription": "Konfigurer innstillingene for din identitetsleverandør", - "roleMapping": "Rolletilordning", - "orgMapping": "Organisasjon Kartlegging", - "orgPoliciesSearch": "Søk i organisasjonens retningslinjer...", - "orgPoliciesAdd": "Legg til organisasjonspolicy", - "orgRequired": "Organisasjon er påkrevd", - "error": "Feil", - "success": "Suksess", - "orgPolicyAddedDescription": "Policy vellykket lagt til", - "orgPolicyUpdatedDescription": "Policyen er vellykket oppdatert", - "orgPolicyDeletedDescription": "Policy slettet vellykket", - "defaultMappingsUpdatedDescription": "Standardtilordninger oppdatert vellykket", - "orgPoliciesAbout": "Om organisasjonens retningslinjer", - "orgPoliciesAboutDescription": "Organisasjonspolicyer brukes til å kontrollere tilgang til organisasjoner basert på brukerens ID-token. Du kan spesifisere JMESPath-uttrykk for å trekke ut rolle- og organisasjonsinformasjon fra ID-tokenet.", - "orgPoliciesAboutDescriptionLink": "Se dokumentasjon, for mer informasjon.", - "defaultMappingsOptional": "Standard Tilordninger (Valgfritt)", - "defaultMappingsOptionalDescription": "Standardtilordningene brukes når det ikke er definert en organisasjonspolicy for en organisasjon. Du kan spesifisere standard rolle- og organisasjonstilordninger som det kan falles tilbake på her.", - "defaultMappingsRole": "Standard rolletilordning", - "defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.", - "defaultMappingsOrg": "Standard organisasjonstilordning", - "defaultMappingsOrgDescription": "Dette uttrykket må returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.", - "defaultMappingsSubmit": "Lagre standard tilordninger", - "orgPoliciesEdit": "Rediger Organisasjonspolicy", - "org": "Organisasjon", - "orgSelect": "Velg organisasjon", - "orgSearch": "Søk organisasjon", - "orgNotFound": "Ingen organisasjon funnet.", - "roleMappingPathOptional": "Rollekartleggingssti (Valgfritt)", - "orgMappingPathOptional": "Organisasjonstilordningssti (Valgfritt)", - "orgPolicyUpdate": "Oppdater policy", - "orgPolicyAdd": "Legg til policy", - "orgPolicyConfig": "Konfigurer tilgang for en organisasjon", - "idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert", - "redirectUrl": "Omdirigerings-URL", - "redirectUrlAbout": "Om omdirigerings-URL", - "redirectUrlAboutDescription": "Dette er URL-en som brukere vil bli omdirigert til etter autentisering. Du må konfigurere denne URL-en i innstillingene for identitetsleverandøren din.", - "pangolinAuth": "Autentisering - Pangolin", - "verificationCodeLengthRequirements": "Din verifiseringskode må være 8 tegn.", - "errorOccurred": "Det oppstod en feil", - "emailErrorVerify": "Kunne ikke verifisere e-post:", - "emailVerified": "E-posten er bekreftet! Omdirigerer deg...", - "verificationCodeErrorResend": "Kunne ikke sende bekreftelseskode på nytt:", - "verificationCodeResend": "Bekreftelseskode sendt på nytt", - "verificationCodeResendDescription": "Vi har sendt en ny bekreftelseskode til e-postadressen din. Vennligst sjekk innboksen din.", - "emailVerify": "Verifiser e-post", - "emailVerifyDescription": "Skriv inn bekreftelseskoden sendt til e-postadressen din.", - "verificationCode": "Verifiseringskode", - "verificationCodeEmailSent": "Vi har sendt en bekreftelseskode til e-postadressen din.", - "submit": "Send inn", - "emailVerifyResendProgress": "Sender på nytt...", - "emailVerifyResend": "Har du ikke mottatt en kode? Klikk her for å sende på nytt", - "passwordNotMatch": "Passordene stemmer ikke", - "signupError": "Det oppsto en feil ved registrering", - "pangolinLogoAlt": "Pangolin Logo", - "inviteAlready": "Ser ut til at du har blitt invitert!", - "inviteAlreadyDescription": "For å godta invitasjonen, må du logge inn eller opprette en konto.", - "signupQuestion": "Har du allerede en konto?", - "login": "Logg inn", - "resourceNotFound": "Ressurs ikke funnet", - "resourceNotFoundDescription": "Ressursen du prøver å få tilgang til eksisterer ikke.", - "pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer", - "pincodeRequirementsChars": "PIN må kun inneholde tall", - "passwordRequirementsLength": "Passord må være minst 1 tegn langt", - "otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.", - "otpEmailSent": "OTP sendt", - "otpEmailSentDescription": "En OTP er sendt til din e-post", - "otpEmailErrorAuthenticate": "Mislyktes å autentisere med e-post", - "pincodeErrorAuthenticate": "Kunne ikke autentisere med pinkode", - "passwordErrorAuthenticate": "Kunne ikke autentisere med passord", - "poweredBy": "Drevet av", - "authenticationRequired": "Autentisering påkrevd", - "authenticationMethodChoose": "Velg din foretrukne metode for å få tilgang til {name}", - "authenticationRequest": "Du må autentisere deg for å få tilgang til {name}", - "user": "Bruker", - "pincodeInput": "6-sifret PIN-kode", - "pincodeSubmit": "Logg inn med PIN", - "passwordSubmit": "Logg inn med passord", - "otpEmailDescription": "En engangskode vil bli sendt til denne e-posten.", - "otpEmailSend": "Send engangskode", - "otpEmail": "Engangspassord (OTP)", - "otpEmailSubmit": "Send inn OTP", - "backToEmail": "Tilbake til E-post", - "noSupportKey": "Serveren kjører uten en supporterlisens. Vurder å støtte prosjektet!", - "accessDenied": "Tilgang nektet", - "accessDeniedDescription": "Du har ikke tilgang til denne ressursen. Hvis dette er en feil, vennligst kontakt administratoren.", - "accessTokenError": "Feil ved sjekk av tilgangstoken", - "accessGranted": "Tilgang gitt", - "accessUrlInvalid": "Ugyldig tilgangs-URL", - "accessGrantedDescription": "Du har fått tilgang til denne ressursen. Omdirigerer deg...", - "accessUrlInvalidDescription": "Denne delings-URL-en er ugyldig. Vennligst kontakt ressurseieren for en ny URL.", - "tokenInvalid": "Ugyldig token", - "pincodeInvalid": "Ugyldig kode", - "passwordErrorRequestReset": "Forespørsel om tilbakestilling mislyktes", - - "passwordErrorReset": "Klarte ikke å tilbakestille passord:", - "passwordResetSuccess": "Passordet er tilbakestilt! Går tilbake til innlogging...", - "passwordReset": "Tilbakestill passord", - "passwordResetDescription": "Følg stegene for å tilbakestille passordet ditt", - "passwordResetSent": "Vi sender en kode for tilbakestilling av passord til denne e-postadressen.", - "passwordResetCode": "Tilbakestillingskode", - "passwordResetCodeDescription": "Sjekk e-posten din for tilbakestillingskoden.", - "passwordNew": "Nytt passord", - "passwordNewConfirm": "Bekreft nytt passord", - "pincodeAuth": "Autentiseringskode", - "pincodeSubmit2": "Send inn kode", - "passwordResetSubmit": "Be om tilbakestilling", - "passwordBack": "Tilbake til passord", - "loginBack": "Gå tilbake til innlogging", - "signup": "Registrer deg", - "loginStart": "Logg inn for å komme i gang", - "idpOidcTokenValidating": "Validerer OIDC-token", - "idpOidcTokenResponse": "Valider OIDC-tokensvar", - "idpErrorOidcTokenValidating": "Feil ved validering av OIDC-token", - "idpConnectingTo": "Kobler til {name}", - "idpConnectingToDescription": "Validerer identiteten din", - "idpConnectingToProcess": "Kobler til...", - "idpConnectingToFinished": "Tilkoblet", - "idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.", - "idpErrorNotFound": "IdP ikke funnet", - "inviteInvalid": "Ugyldig invitasjon", - "inviteInvalidDescription": "Invitasjonslenken er ugyldig.", - "inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren", - "inviteErrorUserNotExists": "Brukeren eksisterer ikke. Vennligst opprett en konto først.", - "inviteErrorLoginRequired": "Du må være logget inn for å godta en invitasjon", - "inviteErrorExpired": "Invitasjonen kan ha utløpt", - "inviteErrorRevoked": "Invitasjonen kan ha blitt trukket tilbake", - "inviteErrorTypo": "Det kan være en skrivefeil i invitasjonslenken", - "pangolinSetup": "Oppsett - Pangolin", - "orgNameRequired": "Organisasjonsnavn er påkrevd", - "orgIdRequired": "Organisasjons-ID er påkrevd", - "orgErrorCreate": "En feil oppstod under oppretting av organisasjon", - "pageNotFound": "Siden ble ikke funnet", - "pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.", - "overview": "Oversikt", - "home": "Hjem", - "accessControl": "Tilgangskontroll", - "settings": "Innstillinger", - "usersAll": "Alle brukere", - "license": "Lisens", - "pangolinDashboard": "Dashbord - Pangolin", - "noResults": "Ingen resultater funnet.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Inntastede tagger", - "tagsEnteredDescription": "Dette er taggene du har tastet inn.", - "tagsWarnCannotBeLessThanZero": "maxTags og minTags kan ikke være mindre enn 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tagg ikke tillatt i henhold til autofullfør-alternativer", - "tagsWarnInvalid": "Ugyldig tagg i henhold til validateTag", - "tagWarnTooShort": "Tagg {tagText} er for kort", - "tagWarnTooLong": "Tagg {tagText} er for lang", - "tagsWarnReachedMaxNumber": "Maksimalt antall tillatte tagger er nådd", - "tagWarnDuplicate": "Duplisert tagg {tagText} ble ikke lagt til", - "supportKeyInvalid": "Ugyldig nøkkel", - "supportKeyInvalidDescription": "Din supporternøkkel er ugyldig.", - "supportKeyValid": "Gyldig nøkkel", - "supportKeyValidDescription": "Din supporternøkkel er validert. Takk for din støtte!", - "supportKeyErrorValidationDescription": "Klarte ikke å validere supporternøkkel.", - "supportKey": "Støtt utviklingen og adopter en Pangolin!", - "supportKeyDescription": "Kjøp en supporternøkkel for å hjelpe oss med å fortsette utviklingen av Pangolin for fellesskapet. Ditt bidrag lar oss bruke mer tid på å vedlikeholde og legge til nye funksjoner i applikasjonen for alle. Vi vil aldri bruke dette til å legge funksjoner bak en betalingsmur. Dette er atskilt fra enhver kommersiell utgave.", - "supportKeyPet": "Du vil også få adoptere og møte din helt egen kjæledyr-Pangolin!", - "supportKeyPurchase": "Betalinger behandles via GitHub. Etterpå kan du hente nøkkelen din på", - "supportKeyPurchaseLink": "vår nettside", - "supportKeyPurchase2": "og løse den inn her.", - "supportKeyLearnMore": "Lær mer.", - "supportKeyOptions": "Vennligst velg det alternativet som passer deg best.", - "supportKetOptionFull": "Full støttespiller", - "forWholeServer": "For hele serveren", - "lifetimePurchase": "Livstidskjøp", - "supporterStatus": "Supporterstatus", - "buy": "Kjøp", - "supportKeyOptionLimited": "Begrenset støttespiller", - "forFiveUsers": "For 5 eller færre brukere", - "supportKeyRedeem": "Løs inn supporternøkkel", - "supportKeyHideSevenDays": "Skjul i 7 dager", - "supportKeyEnter": "Skriv inn supporternøkkel", - "supportKeyEnterDescription": "Møt din helt egen kjæledyr-Pangolin!", - "githubUsername": "GitHub-brukernavn", - "supportKeyInput": "Supporternøkkel", - "supportKeyBuy": "Kjøp supporternøkkel", - "logoutError": "Feil ved utlogging", - "signingAs": "Logget inn som", - "serverAdmin": "Serveradministrator", - "otpEnable": "Aktiver tofaktor", - "otpDisable": "Deaktiver tofaktor", - "logout": "Logg ut", - "licenseTierProfessionalRequired": "Profesjonell utgave påkrevd", - "licenseTierProfessionalRequiredDescription": "Denne funksjonen er kun tilgjengelig i den profesjonelle utgaven.", - "actionGetOrg": "Hent organisasjon", - "actionUpdateOrg": "Oppdater organisasjon", - "actionUpdateUser": "Oppdater bruker", - "actionGetUser": "Hent bruker", - "actionGetOrgUser": "Hent organisasjonsbruker", - "actionListOrgDomains": "List opp organisasjonsdomener", - "actionCreateSite": "Opprett område", - "actionDeleteSite": "Slett område", - "actionGetSite": "Hent område", - "actionListSites": "List opp områder", - "actionUpdateSite": "Oppdater område", - "actionListSiteRoles": "List opp tillatte områderoller", - "actionCreateResource": "Opprett ressurs", - "actionDeleteResource": "Slett ressurs", - "actionGetResource": "Hent ressurs", - "actionListResource": "List opp ressurser", - "actionUpdateResource": "Oppdater ressurs", - "actionListResourceUsers": "List opp ressursbrukere", - "actionSetResourceUsers": "Angi ressursbrukere", - "actionSetAllowedResourceRoles": "Angi tillatte ressursroller", - "actionListAllowedResourceRoles": "List opp tillatte ressursroller", - "actionSetResourcePassword": "Angi ressurspassord", - "actionSetResourcePincode": "Angi ressurspinkode", - "actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs", - "actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs", - "actionCreateTarget": "Opprett mål", - "actionDeleteTarget": "Slett mål", - "actionGetTarget": "Hent mål", - "actionListTargets": "List opp mål", - "actionUpdateTarget": "Oppdater mål", - "actionCreateRole": "Opprett rolle", - "actionDeleteRole": "Slett rolle", - "actionGetRole": "Hent rolle", - "actionListRole": "List opp roller", - "actionUpdateRole": "Oppdater rolle", - "actionListAllowedRoleResources": "List opp tillatte rolleressurser", - "actionInviteUser": "Inviter bruker", - "actionRemoveUser": "Fjern bruker", - "actionListUsers": "List opp brukere", - "actionAddUserRole": "Legg til brukerrolle", - "actionGenerateAccessToken": "Generer tilgangstoken", - "actionDeleteAccessToken": "Slett tilgangstoken", - "actionListAccessTokens": "List opp tilgangstokener", - "actionCreateResourceRule": "Opprett ressursregel", - "actionDeleteResourceRule": "Slett ressursregel", - "actionListResourceRules": "List opp ressursregler", - "actionUpdateResourceRule": "Oppdater ressursregel", - "actionListOrgs": "List opp organisasjoner", - "actionCheckOrgId": "Sjekk ID", - "actionCreateOrg": "Opprett organisasjon", - "actionDeleteOrg": "Slett organisasjon", - "actionListApiKeys": "List opp API-nøkler", - "actionListApiKeyActions": "List opp API-nøkkelhandlinger", - "actionSetApiKeyActions": "Angi tillatte handlinger for API-nøkkel", - "actionCreateApiKey": "Opprett API-nøkkel", - "actionDeleteApiKey": "Slett API-nøkkel", - "actionCreateIdp": "Opprett IDP", - "actionUpdateIdp": "Oppdater IDP", - "actionDeleteIdp": "Slett IDP", - "actionListIdps": "List opp IDP-er", - "actionGetIdp": "Hent IDP", - "actionCreateIdpOrg": "Opprett IDP-organisasjonspolicy", - "actionDeleteIdpOrg": "Slett IDP-organisasjonspolicy", - "actionListIdpOrgs": "List opp IDP-organisasjoner", - "actionUpdateIdpOrg": "Oppdater IDP-organisasjon", - "noneSelected": "Ingen valgt", - "orgNotFound2": "Ingen organisasjoner funnet.", - "searchProgress": "Søker...", - "create": "Opprett", - "orgs": "Organisasjoner", - "loginError": "En feil oppstod under innlogging", - "passwordForgot": "Glemt passordet ditt?", - "otpAuth": "Tofaktorautentisering", - "otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.", - "otpAuthSubmit": "Send inn kode", - "idpContinue": "Eller fortsett med", - "otpAuthBack": "Tilbake til innlogging", - "navbar": "Navigasjonsmeny", - "navbarDescription": "Hovednavigasjonsmeny for applikasjonen", - "navbarDocsLink": "Dokumentasjon", - "commercialEdition": "Kommersiell utgave", - "otpErrorEnable": "Kunne ikke aktivere 2FA", - "otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA", - "otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode", - "otpSetupCheckCodeRetry": "Ugyldig kode. Vennligst prøv igjen.", - "otpSetup": "Aktiver tofaktorautentisering", - "otpSetupDescription": "Sikre kontoen din med et ekstra lag med beskyttelse", - "otpSetupScanQr": "Skann denne QR-koden med autentiseringsappen din eller skriv inn den hemmelige nøkkelen manuelt:", - "otpSetupSecretCode": "Autentiseringskode", - "otpSetupSuccess": "Tofaktorautentisering aktivert", - "otpSetupSuccessStoreBackupCodes": "Kontoen din er nå sikrere. Ikke glem å lagre reservekodene dine.", - "otpErrorDisable": "Kunne ikke deaktivere 2FA", - "otpErrorDisableDescription": "En feil oppstod under deaktivering av 2FA", - "otpRemove": "Deaktiver tofaktorautentisering", - "otpRemoveDescription": "Deaktiver tofaktorautentisering for kontoen din", - "otpRemoveSuccess": "Tofaktorautentisering deaktivert", - "otpRemoveSuccessMessage": "Tofaktorautentisering er deaktivert for kontoen din. Du kan aktivere den igjen når som helst.", - "otpRemoveSubmit": "Deaktiver 2FA", - "paginator": "Side {current} av {last}", - "paginatorToFirst": "Gå til første side", - "paginatorToPrevious": "Gå til forrige side", - "paginatorToNext": "Gå til neste side", - "paginatorToLast": "Gå til siste side", - "copyText": "Kopier tekst", - "copyTextFailed": "Klarte ikke å kopiere tekst: ", - "copyTextClipboard": "Kopier til utklippstavle", - "inviteErrorInvalidConfirmation": "Ugyldig bekreftelse", - "passwordRequired": "Passord er påkrevd", - "allowAll": "Tillat alle", - "permissionsAllowAll": "Tillat alle rettigheter", - "githubUsernameRequired": "GitHub-brukernavn er påkrevd", - "supportKeyRequired": "supporternøkkel er påkrevd", - "passwordRequirementsChars": "Passordet må være minst 8 tegn", - "language": "Språk", - "verificationCodeRequired": "Kode er påkrevd", - "userErrorNoUpdate": "Ingen bruker å oppdatere", - "siteErrorNoUpdate": "Ingen område å oppdatere", - "resourceErrorNoUpdate": "Ingen ressurs å oppdatere", - "authErrorNoUpdate": "Ingen autentiseringsinfo å oppdatere", - "orgErrorNoUpdate": "Ingen organisasjon å oppdatere", - "orgErrorNoProvided": "Ingen organisasjon angitt", - "apiKeysErrorNoUpdate": "Ingen API-nøkkel å oppdatere", - "sidebarOverview": "Oversikt", - "sidebarHome": "Hjem", - "sidebarSites": "Områder", - "sidebarResources": "Ressurser", - "sidebarAccessControl": "Tilgangskontroll", - "sidebarUsers": "Brukere", - "sidebarInvitations": "Invitasjoner", - "sidebarRoles": "Roller", - "sidebarShareableLinks": "Delbare lenker", - "sidebarApiKeys": "API-nøkler", - "sidebarSettings": "Innstillinger", - "sidebarAllUsers": "Alle brukere", - "sidebarIdentityProviders": "Identitetsleverandører", - "sidebarLicense": "Lisens", - "sidebarClients": "Klienter (Beta)", - "sidebarDomains": "Domener", - "enableDockerSocket": "Aktiver Docker Socket", - "enableDockerSocketDescription": "Aktiver Docker Socket-oppdagelse for å fylle ut containerinformasjon. Socket-stien må oppgis til Newt.", - "enableDockerSocketLink": "Lær mer", - "viewDockerContainers": "Vis Docker-containere", - "containersIn": "Containere i {siteName}", - "selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.", - "containerName": "Navn", - "containerImage": "Bilde", - "containerState": "Tilstand", - "containerNetworks": "Nettverk", - "containerHostnameIp": "Vertsnavn/IP", - "containerLabels": "Etiketter", - "containerLabelsCount": "{count, plural, en {# etikett} other {# etiketter}}", - "containerLabelsTitle": "Containeretiketter", - "containerLabelEmpty": "", - "containerPorts": "Porter", - "containerPortsMore": "+{count} til", - "containerActions": "Handlinger", - "select": "Velg", - "noContainersMatchingFilters": "Ingen containere funnet som matcher de nåværende filtrene.", - "showContainersWithoutPorts": "Vis containere uten porter", - "showStoppedContainers": "Vis stoppede containere", - "noContainersFound": "Ingen containere funnet. Sørg for at Docker-containere kjører.", - "searchContainersPlaceholder": "Søk blant {count} containere...", - "searchResultsCount": "{count, plural, en {# resultat} other {# resultater}}", - "filters": "Filtre", - "filterOptions": "Filteralternativer", - "filterPorts": "Porter", - "filterStopped": "Stoppet", - "clearAllFilters": "Fjern alle filtre", - "columns": "Kolonner", - "toggleColumns": "Vis/skjul kolonner", - "refreshContainersList": "Oppdater containerliste", - "searching": "Søker...", - "noContainersFoundMatching": "Ingen containere funnet som matcher \"{filter}\".", - "light": "lys", - "dark": "mørk", - "system": "system", - "theme": "Tema", - "subnetRequired": "Subnett er påkrevd", - "initialSetupTitle": "Førstegangsoppsett av server", - "initialSetupDescription": "Opprett den første serveradministratorkontoen. Det kan bare finnes én serveradministrator. Du kan alltid endre denne påloggingsinformasjonen senere.", - "createAdminAccount": "Opprett administratorkonto", - "setupErrorCreateAdmin": "En feil oppstod under opprettelsen av serveradministratorkontoen.", - "certificateStatus": "Sertifikatstatus", - "loading": "Laster inn", - "restart": "Start på nytt", - "domains": "Domener", - "domainsDescription": "Administrer domener for organisasjonen din", - "domainsSearch": "Søk i domener...", - "domainAdd": "Legg til domene", - "domainAddDescription": "Registrer et nytt domene hos organisasjonen din", - "domainCreate": "Opprett domene", - "domainCreatedDescription": "Domene ble opprettet", - "domainDeletedDescription": "Domene ble slettet", - "domainQuestionRemove": "Er du sikker på at du vil fjerne domenet {domain} fra kontoen din?", - "domainMessageRemove": "Når domenet er fjernet, vil det ikke lenger være knyttet til kontoen din.", - "domainMessageConfirm": "For å bekrefte, vennligst skriv inn domenenavnet nedenfor.", - "domainConfirmDelete": "Bekreft sletting av domene", - "domainDelete": "Slett domene", - "domain": "Domene", - "selectDomainTypeNsName": "Domenedelegering (NS)", - "selectDomainTypeNsDescription": "Dette domenet og alle dets underdomener. Bruk dette når du vil kontrollere en hel domenesone.", - "selectDomainTypeCnameName": "Enkelt domene (CNAME)", - "selectDomainTypeCnameDescription": "Bare dette spesifikke domenet. Bruk dette for individuelle underdomener eller spesifikke domeneoppføringer.", - "selectDomainTypeWildcardName": "Wildcard-domene", - "selectDomainTypeWildcardDescription": "Dette domenet og dets underdomener.", - "domainDelegation": "Enkelt domene", - "selectType": "Velg en type", - "actions": "Handlinger", - "refresh": "Oppdater", - "refreshError": "Klarte ikke å oppdatere data", - "verified": "Verifisert", - "pending": "Venter", - "sidebarBilling": "Fakturering", - "billing": "Fakturering", - "orgBillingDescription": "Administrer faktureringsinformasjon og abonnementer", - "github": "GitHub", - "pangolinHosted": "Driftet av Pangolin", - "fossorial": "Fossorial", - "completeAccountSetup": "Fullfør kontooppsett", - "completeAccountSetupDescription": "Angi passordet ditt for å komme i gang", - "accountSetupSent": "Vi sender en oppsettskode for kontoen til denne e-postadressen.", - "accountSetupCode": "Oppsettskode", - "accountSetupCodeDescription": "Sjekk e-posten din for oppsettskoden.", - "passwordCreate": "Opprett passord", - "passwordCreateConfirm": "Bekreft passord", - "accountSetupSubmit": "Send oppsettskode", - "completeSetup": "Fullfør oppsett", - "accountSetupSuccess": "Kontooppsett fullført! Velkommen til Pangolin!", - "documentation": "Dokumentasjon", - "saveAllSettings": "Lagre alle innstillinger", - "settingsUpdated": "Innstillinger oppdatert", - "settingsUpdatedDescription": "Alle innstillinger er oppdatert", - "settingsErrorUpdate": "Klarte ikke å oppdatere innstillinger", - "settingsErrorUpdateDescription": "En feil oppstod under oppdatering av innstillinger", - "sidebarCollapse": "Skjul", - "sidebarExpand": "Utvid", - "newtUpdateAvailable": "Oppdatering tilgjengelig", - "newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.", - "domainPickerEnterDomain": "Domene", - "domainPickerPlaceholder": "minapp.eksempel.com, api.v1.mittdomene.com, eller bare minapp", - "domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.", - "domainPickerDescriptionSaas": "Skriv inn et fullt domene, underdomene eller bare et navn for å se tilgjengelige alternativer", - "domainPickerTabAll": "Alle", - "domainPickerTabOrganization": "Organisasjon", - "domainPickerTabProvided": "Levert", - "domainPickerSortAsc": "A-Å", - "domainPickerSortDesc": "Å-A", - "domainPickerCheckingAvailability": "Sjekker tilgjengelighet...", - "domainPickerNoMatchingDomains": "Ingen samsvarende domener funnet. Prøv et annet domene eller sjekk organisasjonens domeneinnstillinger.", - "domainPickerOrganizationDomains": "Organisasjonsdomener", - "domainPickerProvidedDomains": "Leverte domener", - "domainPickerSubdomain": "Underdomene: {subdomain}", - "domainPickerNamespace": "Navnerom: {namespace}", - "domainPickerShowMore": "Vis mer", - "domainNotFound": "Domene ikke funnet", - "domainNotFoundDescription": "Denne ressursen er deaktivert fordi domenet ikke lenger eksisterer i systemet vårt. Vennligst angi et nytt domene for denne ressursen.", - "failed": "Mislyktes", - "createNewOrgDescription": "Opprett en ny organisasjon", - "organization": "Organisasjon", - "port": "Port", - "securityKeyManage": "Administrer sikkerhetsnøkler", - "securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering", - "securityKeyRegister": "Registrer ny sikkerhetsnøkkel", - "securityKeyList": "Dine sikkerhetsnøkler", - "securityKeyNone": "Ingen sikkerhetsnøkler er registrert enda", - "securityKeyNameRequired": "Navn er påkrevd", - "securityKeyRemove": "Fjern", - "securityKeyLastUsed": "Sist brukt: {date}", - "securityKeyNameLabel": "Navn på sikkerhetsnøkkel", - "securityKeyRegisterSuccess": "Sikkerhetsnøkkel registrert", - "securityKeyRegisterError": "Klarte ikke å registrere sikkerhetsnøkkel", - "securityKeyRemoveSuccess": "Sikkerhetsnøkkel fjernet", - "securityKeyRemoveError": "Klarte ikke å fjerne sikkerhetsnøkkel", - "securityKeyLoadError": "Klarte ikke å laste inn sikkerhetsnøkler", - "securityKeyLogin": "Fortsett med sikkerhetsnøkkel", - "securityKeyAuthError": "Klarte ikke å autentisere med sikkerhetsnøkkel", - "securityKeyRecommendation": "Registrer en reservesikkerhetsnøkkel på en annen enhet for å sikre at du alltid har tilgang til kontoen din.", - "registering": "Registrerer...", - "securityKeyPrompt": "Vennligst verifiser identiteten din med sikkerhetsnøkkelen. Sørg for at sikkerhetsnøkkelen er koblet til og klar.", - "securityKeyBrowserNotSupported": "Nettleseren din støtter ikke sikkerhetsnøkler. Vennligst bruk en moderne nettleser som Chrome, Firefox eller Safari.", - "securityKeyPermissionDenied": "Vennligst tillat tilgang til sikkerhetsnøkkelen din for å fortsette innloggingen.", - "securityKeyRemovedTooQuickly": "Vennligst hold sikkerhetsnøkkelen tilkoblet til innloggingsprosessen er fullført.", - "securityKeyNotSupported": "Sikkerhetsnøkkelen din er kanskje ikke kompatibel. Vennligst prøv en annen sikkerhetsnøkkel.", - "securityKeyUnknownError": "Det oppstod et problem med å bruke sikkerhetsnøkkelen din. Vennligst prøv igjen.", - "twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.", - "twoFactor": "Tofaktorautentisering", - "adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.", - "continueToApplication": "Fortsett til applikasjonen", - "securityKeyAdd": "Legg til sikkerhetsnøkkel", - "securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel", - "securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den", - "securityKeyTwoFactorRequired": "Tofaktorautentisering påkrevd", - "securityKeyTwoFactorDescription": "Vennligst skriv inn koden for tofaktorautentisering for å registrere sikkerhetsnøkkelen", - "securityKeyTwoFactorRemoveDescription": "Vennligst skriv inn koden for tofaktorautentisering for å fjerne sikkerhetsnøkkelen", - "securityKeyTwoFactorCode": "Tofaktorkode", - "securityKeyRemoveTitle": "Fjern sikkerhetsnøkkel", - "securityKeyRemoveDescription": "Skriv inn passordet ditt for å fjerne sikkerhetsnøkkelen \"{name}\"", - "securityKeyNoKeysRegistered": "Ingen sikkerhetsnøkler registrert", - "securityKeyNoKeysDescription": "Legg til en sikkerhetsnøkkel for å øke sikkerheten på kontoen din", - "createDomainRequired": "Domene er påkrevd", - "createDomainAddDnsRecords": "Legg til DNS-oppføringer", - "createDomainAddDnsRecordsDescription": "Legg til følgende DNS-oppføringer hos din domeneleverandør for å fullføre oppsettet.", - "createDomainNsRecords": "NS-oppføringer", - "createDomainRecord": "Oppføring", - "createDomainType": "Type:", - "createDomainName": "Navn:", - "createDomainValue": "Verdi:", - "createDomainCnameRecords": "CNAME-oppføringer", - "createDomainARecords": "A-oppføringer", - "createDomainRecordNumber": "Oppføring {number}", - "createDomainTxtRecords": "TXT-oppføringer", - "createDomainSaveTheseRecords": "Lagre disse oppføringene", - "createDomainSaveTheseRecordsDescription": "Sørg for å lagre disse DNS-oppføringene, da du ikke vil se dem igjen.", - "createDomainDnsPropagation": "DNS-propagering", - "createDomainDnsPropagationDescription": "DNS-endringer kan ta litt tid å propagere over internett. Dette kan ta fra noen få minutter til 48 timer, avhengig av din DNS-leverandør og TTL-innstillinger.", - "resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser", - "resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser", - "signUpTerms": { - "IAgreeToThe": "Jeg godtar", - "termsOfService": "brukervilkårene", - "and": "og", - "privacyPolicy": "personvernerklæringen" - }, - "siteRequired": "Område er påkrevd.", - "olmTunnel": "Olm-tunnel", - "olmTunnelDescription": "Bruk Olm for klienttilkobling", - "errorCreatingClient": "Feil ved oppretting av klient", - "clientDefaultsNotFound": "Klientstandarder ikke funnet", - "createClient": "Opprett klient", - "createClientDescription": "Opprett en ny klient for å koble til dine områder", - "seeAllClients": "Se alle klienter", - "clientInformation": "Klientinformasjon", - "clientNamePlaceholder": "Klientnavn", - "address": "Adresse", - "subnetPlaceholder": "Subnett", - "addressDescription": "Adressen denne klienten vil bruke for tilkobling", - "selectSites": "Velg områder", - "sitesDescription": "Klienten vil ha tilkobling til de valgte områdene", - "clientInstallOlm": "Installer Olm", - "clientInstallOlmDescription": "Få Olm til å kjøre på systemet ditt", - "clientOlmCredentials": "Olm-legitimasjon", - "clientOlmCredentialsDescription": "Slik vil Olm autentisere med serveren", - "olmEndpoint": "Olm-endepunkt", - "olmId": "Olm-ID", - "olmSecretKey": "Olm hemmelig nøkkel", - "clientCredentialsSave": "Lagre din legitimasjon", - "clientCredentialsSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", - "generalSettingsDescription": "Konfigurer de generelle innstillingene for denne klienten", - "clientUpdated": "Klient oppdatert", - "clientUpdatedDescription": "Klienten er blitt oppdatert.", - "clientUpdateFailed": "Klarte ikke å oppdatere klient", - "clientUpdateError": "En feil oppstod under oppdatering av klienten.", - "sitesFetchFailed": "Klarte ikke å hente områder", - "sitesFetchError": "En feil oppstod under henting av områder.", - "olmErrorFetchReleases": "En feil oppstod under henting av Olm-utgivelser.", - "olmErrorFetchLatest": "En feil oppstod under henting av den nyeste Olm-utgivelsen.", - "remoteSubnets": "Fjern-subnett", - "enterCidrRange": "Skriv inn CIDR-område", - "remoteSubnetsDescription": "Legg til CIDR-områder som kan få fjerntilgang til dette området. Bruk format som 10.0.0.0/24 eller 192.168.1.0/24.", - "resourceEnableProxy": "Aktiver offentlig proxy", - "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", - "externalProxyEnabled": "Ekstern proxy aktivert" -} \ No newline at end of file + "setupCreate": "Lag din organisasjon, område og dine ressurser", + "setupNewOrg": "Ny Organisasjon", + "setupCreateOrg": "Opprett organisasjon", + "setupCreateResources": "Opprett ressurser", + "setupOrgName": "Organisasjonsnavn", + "orgDisplayName": "Dette er visningsnavnet til organisasjonen din.", + "orgId": "Organisasjons-ID", + "setupIdentifierMessage": "Dette er den unike identifikator for din organisasjon. Dette er separat fra visningsnavnet.", + "setupErrorIdentifier": "Organisasjons-ID er allerede tatt. Vennligst velg en annen.", + "componentsErrorNoMemberCreate": "Du er for øyeblikket ikke medlem av noen organisasjoner. Lag en organisasjon for å komme i gang.", + "componentsErrorNoMember": "Du er for øyeblikket ikke medlem av noen organisasjoner.", + "welcome": "Velkommen!", + "welcomeTo": "Velkommen til", + "componentsCreateOrg": "Lag en Organisasjon", + "componentsMember": "Du er {count, plural, =0 {ikke medlem av noen organisasjoner} one {medlem av en organisasjon} other {medlem av # organisasjoner}}.", + "componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", + "dismiss": "Avvis", + "componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", + "componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!", + "inviteErrorNotValid": "Beklager, men det ser ut som invitasjonen du prøver å bruke ikke har blitt akseptert eller ikke er gyldig lenger.", + "inviteErrorUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til, ikke er for denne brukeren.", + "inviteLoginUser": "Vennligst sjekk at du er logget inn som riktig bruker.", + "inviteErrorNoUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til ikke er for en bruker som eksisterer.", + "inviteCreateUser": "Vennligst opprett en konto først.", + "goHome": "Gå hjem", + "inviteLogInOtherUser": "Logg inn som en annen bruker", + "createAnAccount": "Lag konto", + "inviteNotAccepted": "Invitasjonen ikke akseptert", + "authCreateAccount": "Opprett en konto for å komme i gang", + "authNoAccount": "Har du ikke konto?", + "email": "E-post", + "password": "Passord", + "confirmPassword": "Bekreft Passord", + "createAccount": "Opprett Konto", + "viewSettings": "Vis Innstillinger", + "delete": "Slett", + "name": "Navn", + "online": "Online", + "offline": "Frakoblet", + "site": "Område", + "dataIn": "Data Inn", + "dataOut": "Data Ut", + "connectionType": "Tilkoblingstype", + "tunnelType": "Tunneltype", + "local": "Lokal", + "edit": "Rediger", + "siteConfirmDelete": "Bekreft Sletting av Område", + "siteDelete": "Slett Område", + "siteMessageRemove": "Når området slettes, vil det ikke lenger være tilgjengelig. Alle ressurser og mål assosiert med området vil også bli slettet.", + "siteMessageConfirm": "For å bekrefte, vennligst skriv inn navnet i området nedenfor.", + "siteQuestionRemove": "Er du sikker på at du vil slette området {selectedSite} fra organisasjonen?", + "siteManageSites": "Administrer Områder", + "siteDescription": "Tillat tilkobling til nettverket ditt gjennom sikre tunneler", + "siteCreate": "Opprett område", + "siteCreateDescription2": "Følg trinnene nedenfor for å opprette og koble til et nytt område", + "siteCreateDescription": "Opprett et nytt område for å begynne å koble til ressursene dine", + "close": "Lukk", + "siteErrorCreate": "Feil ved oppretting av område", + "siteErrorCreateKeyPair": "Nøkkelpar eller standardinnstillinger for område ikke funnet", + "siteErrorCreateDefaults": "Standardinnstillinger for område ikke funnet", + "method": "Metode", + "siteMethodDescription": "Slik eksponerer du tilkoblinger.", + "siteLearnNewt": "Lær hvordan du installerer Newt på systemet ditt", + "siteSeeConfigOnce": "Du kan kun se konfigurasjonen én gang.", + "siteLoadWGConfig": "Laster WireGuard-konfigurasjon...", + "siteDocker": "Utvid for detaljer om Docker-deployment", + "toggle": "Veksle", + "dockerCompose": "Docker Compose", + "dockerRun": "Docker Run", + "siteLearnLocal": "Lokale områder tunnelerer ikke, lær mer", + "siteConfirmCopy": "Jeg har kopiert konfigurasjonen", + "searchSitesProgress": "Søker i områder...", + "siteAdd": "Legg til område", + "siteInstallNewt": "Installer Newt", + "siteInstallNewtDescription": "Få Newt til å kjøre på systemet ditt", + "WgConfiguration": "WireGuard Konfigurasjon", + "WgConfigurationDescription": "Bruk følgende konfigurasjon for å koble til nettverket ditt", + "operatingSystem": "Operativsystem", + "commands": "Kommandoer", + "recommended": "Anbefalt", + "siteNewtDescription": "For den beste brukeropplevelsen, bruk Newt. Den bruker WireGuard i bakgrunnen og lar deg adressere dine private ressurser med deres LAN-adresse på ditt private nettverk fra Pangolin-dashbordet.", + "siteRunsInDocker": "Kjører i Docker", + "siteRunsInShell": "Kjører i skall på macOS, Linux og Windows", + "siteErrorDelete": "Feil ved sletting av området", + "siteErrorUpdate": "Klarte ikke å oppdatere området", + "siteErrorUpdateDescription": "En feil oppstod under oppdatering av området.", + "siteUpdated": "Område oppdatert", + "siteUpdatedDescription": "Området har blitt oppdatert.", + "siteGeneralDescription": "Konfigurer de generelle innstillingene for dette området", + "siteSettingDescription": "Konfigurer innstillingene for området ditt", + "siteSetting": "{siteName} Innstillinger", + "siteNewtTunnel": "Newt Tunnel (Anbefalt)", + "siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.", + "siteWg": "Grunnleggende WireGuard", + "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", + "siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.", + "siteSeeAll": "Se alle områder", + "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", + "siteNewtCredentials": "Newt påloggingsinformasjon", + "siteNewtCredentialsDescription": "Slik vil Newt autentisere seg mot serveren", + "siteCredentialsSave": "Lagre påloggingsinformasjonen din", + "siteCredentialsSaveDescription": "Du vil kun kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", + "siteInfo": "Områdeinformasjon", + "status": "Status", + "shareTitle": "Administrer delingslenker", + "shareDescription": "Opprett delbare lenker for å gi midlertidig eller permanent tilgang til ressursene dine", + "shareSearch": "Søk delingslenker...", + "shareCreate": "Opprett delingslenke", + "shareErrorDelete": "Klarte ikke å slette lenke", + "shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke", + "shareDeleted": "Lenke slettet", + "shareDeletedDescription": "Lenken har blitt slettet", + "shareTokenDescription": "Din tilgangsnøkkel kan sendes på to måter: som en query parameter eller i request headers. Disse må sendes fra klienten på hver forespørsel for autentisert tilgang.", + "accessToken": "Tilgangsnøkkel", + "usageExamples": "Brukseksempler", + "tokenId": "Token-ID", + "requestHeades": "Request Headers", + "queryParameter": "Query Parameter", + "importantNote": "Viktig merknad", + "shareImportantDescription": "Av sikkerhetsgrunner anbefales det å bruke headere fremfor query parametere der det er mulig, da query parametere kan logges i serverlogger eller nettleserhistorikk.", + "token": "Token", + "shareTokenSecurety": "Hold tilgangsnøkkelen ditt sikkert. Ikke del i offentlig tilgjengelige områder eller klientkode.", + "shareErrorFetchResource": "Klarte ikke å hente ressurser", + "shareErrorFetchResourceDescription": "En feil oppstod under henting av ressursene", + "shareErrorCreate": "Mislyktes med å opprette delingslenke", + "shareErrorCreateDescription": "Det oppsto en feil ved opprettelse av delingslenken", + "shareCreateDescription": "Alle med denne lenken får tilgang til ressursen", + "shareTitleOptional": "Tittel (valgfritt)", + "expireIn": "Utløper om", + "neverExpire": "Utløper aldri", + "shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.", + "shareSeeOnce": "Du får bare se denne lenken én gang. Pass på å kopiere den.", + "shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.", + "shareTokenUsage": "Se tilgangstokenbruk", + "createLink": "Opprett lenke", + "resourcesNotFound": "Ingen ressurser funnet", + "resourceSearch": "Søk i ressurser", + "openMenu": "Åpne meny", + "resource": "Ressurs", + "title": "Tittel", + "created": "Opprettet", + "expires": "Utløper", + "never": "Aldri", + "shareErrorSelectResource": "Vennligst velg en ressurs", + "resourceTitle": "Administrer Ressurser", + "resourceDescription": "Opprett sikre proxyer til dine private applikasjoner", + "resourcesSearch": "Søk i ressurser...", + "resourceAdd": "Legg til ressurs", + "resourceErrorDelte": "Feil ved sletting av ressurs", + "authentication": "Autentisering", + "protected": "Beskyttet", + "notProtected": "Ikke beskyttet", + "resourceMessageRemove": "Når den er fjernet, vil ressursen ikke lenger være tilgjengelig. Alle mål knyttet til ressursen vil også bli fjernet.", + "resourceMessageConfirm": "For å bekrefte, skriv inn navnet på ressursen nedenfor.", + "resourceQuestionRemove": "Er du sikker på at du vil fjerne ressursen {selectedResource} fra organisasjonen?", + "resourceHTTP": "HTTPS-ressurs", + "resourceHTTPDescription": "Proxy-forespørsler til appen din over HTTPS ved bruk av et underdomene eller grunndomene.", + "resourceRaw": "Rå TCP/UDP-ressurs", + "resourceRawDescription": "Proxyer forespørsler til appen din over TCP/UDP ved å bruke et portnummer.", + "resourceCreate": "Opprett ressurs", + "resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs", + "resourceSeeAll": "Se alle ressurser", + "resourceInfo": "Ressursinformasjon", + "resourceNameDescription": "Dette er visningsnavnet for ressursen.", + "siteSelect": "Velg område", + "siteSearch": "Søk i område", + "siteNotFound": "Ingen område funnet.", + "siteSelectionDescription": "Dette området vil gi tilkobling til ressursen.", + "resourceType": "Ressurstype", + "resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din", + "resourceHTTPSSettings": "HTTPS-innstillinger", + "resourceHTTPSSettingsDescription": "Konfigurer tilgang til ressursen din over HTTPS", + "domainType": "Domenetype", + "subdomain": "Underdomene", + "baseDomain": "Grunndomene", + "subdomnainDescription": "Underdomenet der ressursen din vil være tilgjengelig.", + "resourceRawSettings": "TCP/UDP-innstillinger", + "resourceRawSettingsDescription": "Konfigurer tilgang til ressursen din over TCP/UDP", + "protocol": "Protokoll", + "protocolSelect": "Velg en protokoll", + "resourcePortNumber": "Portnummer", + "resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.", + "cancel": "Avbryt", + "resourceConfig": "Konfigurasjonsutdrag", + "resourceConfigDescription": "Kopier og lim inn disse konfigurasjonsutdragene for å sette opp din TCP/UDP-ressurs", + "resourceAddEntrypoints": "Traefik: Legg til inngangspunkter", + "resourceExposePorts": "Gerbil: Eksponer Porter i Docker Compose", + "resourceLearnRaw": "Lær hvordan å konfigurere TCP/UDP-ressurser", + "resourceBack": "Tilbake til ressurser", + "resourceGoTo": "Gå til ressurs", + "resourceDelete": "Slett ressurs", + "resourceDeleteConfirm": "Bekreft sletting av ressurs", + "visibility": "Synlighet", + "enabled": "Aktivert", + "disabled": "Deaktivert", + "general": "Generelt", + "generalSettings": "Generelle innstillinger", + "proxy": "Proxy", + "rules": "Regler", + "resourceSettingDescription": "Konfigurer innstillingene på ressursen din", + "resourceSetting": "{resourceName} Innstillinger", + "alwaysAllow": "Alltid tillat", + "alwaysDeny": "Alltid avslå", + "orgSettingsDescription": "Konfigurer organisasjonens generelle innstillinger", + "orgGeneralSettings": "Organisasjonsinnstillinger", + "orgGeneralSettingsDescription": "Administrer dine organisasjonsdetaljer og konfigurasjon", + "saveGeneralSettings": "Lagre generelle innstillinger", + "saveSettings": "Lagre innstillinger", + "orgDangerZone": "Faresone", + "orgDangerZoneDescription": "Når du sletter denne organisasjonen er det ingen vei tilbake. Vennligst vær sikker.", + "orgDelete": "Slett organisasjon", + "orgDeleteConfirm": "Bekreft Sletting av Organisasjon", + "orgMessageRemove": "Denne handlingen er irreversibel og vil slette alle tilknyttede data.", + "orgMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på organisasjonen nedenfor.", + "orgQuestionRemove": "Er du sikker på at du vil fjerne organisasjonen {selectedOrg}?", + "orgUpdated": "Organisasjon oppdatert", + "orgUpdatedDescription": "Organisasjonen har blitt oppdatert.", + "orgErrorUpdate": "Kunne ikke oppdatere organisasjonen", + "orgErrorUpdateMessage": "En feil oppsto under oppdatering av organisasjonen.", + "orgErrorFetch": "Klarte ikke å hente organisasjoner", + "orgErrorFetchMessage": "Det oppstod en feil under opplisting av organisasjonene dine", + "orgErrorDelete": "Klarte ikke å slette organisasjon", + "orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.", + "orgDeleted": "Organisasjon slettet", + "orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.", + "orgMissing": "Organisasjons-ID Mangler", + "orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.", + "accessUsersManage": "Administrer brukere", + "accessUsersDescription": "Inviter brukere og gi dem roller for å administrere tilgang til organisasjonen din", + "accessUsersSearch": "Søk etter brukere...", + "accessUserCreate": "Opprett bruker", + "accessUserRemove": "Fjern bruker", + "username": "Brukernavn", + "identityProvider": "Identitetsleverandør", + "role": "Rolle", + "nameRequired": "Navn er påkrevd", + "accessRolesManage": "Administrer Roller", + "accessRolesDescription": "Konfigurer roller for å administrere tilgang til organisasjonen din", + "accessRolesSearch": "Søk etter roller...", + "accessRolesAdd": "Legg til rolle", + "accessRoleDelete": "Slett rolle", + "description": "Beskrivelse", + "inviteTitle": "Åpne invitasjoner", + "inviteDescription": "Administrer invitasjonene dine til andre brukere", + "inviteSearch": "Søk i invitasjoner...", + "minutes": "Minutter", + "hours": "Timer", + "days": "Dager", + "weeks": "Uker", + "months": "Måneder", + "years": "År", + "day": "{count, plural, one {en dag} other {# dager}}", + "apiKeysTitle": "API-nøkkel informasjon", + "apiKeysConfirmCopy2": "Du må bekrefte at du har kopiert API-nøkkelen.", + "apiKeysErrorCreate": "Feil ved oppretting av API-nøkkel", + "apiKeysErrorSetPermission": "Feil ved innstilling av tillatelser", + "apiKeysCreate": "Generer API-nøkkel", + "apiKeysCreateDescription": "Generer en ny API-nøkkel for din organisasjon", + "apiKeysGeneralSettings": "Tillatelser", + "apiKeysGeneralSettingsDescription": "Finn ut hva denne API-nøkkelen kan gjøre", + "apiKeysList": "Din API-nøkkel", + "apiKeysSave": "Lagre API-nøkkelen din", + "apiKeysSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", + "apiKeysInfo": "Din API-nøkkel er:", + "apiKeysConfirmCopy": "Jeg har kopiert API-nøkkelen", + "generate": "Generer", + "done": "Ferdig", + "apiKeysSeeAll": "Se alle API-nøkler", + "apiKeysPermissionsErrorLoadingActions": "Feil ved innlasting av API-nøkkel handlinger", + "apiKeysPermissionsErrorUpdate": "Feil ved innstilling av tillatelser", + "apiKeysPermissionsUpdated": "Tillatelser oppdatert", + "apiKeysPermissionsUpdatedDescription": "Tillatelsene har blitt oppdatert.", + "apiKeysPermissionsGeneralSettings": "Tillatelser", + "apiKeysPermissionsGeneralSettingsDescription": "Bestem hva denne API-nøkkelen kan gjøre", + "apiKeysPermissionsSave": "Lagre tillatelser", + "apiKeysPermissionsTitle": "Tillatelser", + "apiKeys": "API-nøkler", + "searchApiKeys": "Søk API-nøkler", + "apiKeysAdd": "Generer API-nøkkel", + "apiKeysErrorDelete": "Feil under sletting av API-nøkkel", + "apiKeysErrorDeleteMessage": "Feil ved sletting av API-nøkkel", + "apiKeysQuestionRemove": "Er du sikker på at du vil fjerne API-nøkkelen {selectedApiKey} fra organisasjonen?", + "apiKeysMessageRemove": "Når den er fjernet, vil API-nøkkelen ikke lenger kunne brukes.", + "apiKeysMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på API-nøkkelen nedenfor.", + "apiKeysDeleteConfirm": "Bekreft sletting av API-nøkkel", + "apiKeysDelete": "Slett API-nøkkel", + "apiKeysManage": "Administrer API-nøkler", + "apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API", + "apiKeysSettings": "{apiKeyName} Innstillinger", + "userTitle": "Administrer alle brukere", + "userDescription": "Vis og administrer alle brukere i systemet", + "userAbount": "Om brukeradministrasjon", + "userAbountDescription": "Denne tabellen viser alle rotbrukerobjekter i systemet. Hver bruker kan tilhøre flere organisasjoner. Å fjerne en bruker fra en organisasjon sletter ikke deres rotbrukerobjekt – de vil forbli i systemet. For å fullstendig fjerne en bruker fra systemet, må du slette deres rotbrukerobjekt ved å bruke slett-handlingen i denne tabellen.", + "userServer": "Serverbrukere", + "userSearch": "Søk serverbrukere...", + "userErrorDelete": "Feil ved sletting av bruker", + "userDeleteConfirm": "Bekreft sletting av bruker", + "userDeleteServer": "Slett bruker fra server", + "userMessageRemove": "Brukeren vil bli fjernet fra alle organisasjoner og vil bli fullstendig fjernet fra serveren.", + "userMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", + "userQuestionRemove": "Er du sikker på at du vil slette {selectedUser} permanent fra serveren?", + "licenseKey": "Lisensnøkkel", + "valid": "Gyldig", + "numberOfSites": "Antall områder", + "licenseKeySearch": "Søk lisensnøkler...", + "licenseKeyAdd": "Legg til lisensnøkkel", + "type": "Type", + "licenseKeyRequired": "Lisensnøkkel er påkrevd", + "licenseTermsAgree": "Du må godta lisensvilkårene", + "licenseErrorKeyLoad": "Feil ved lasting av lisensnøkler", + "licenseErrorKeyLoadDescription": "Det oppstod en feil ved lasting av lisensnøkler.", + "licenseErrorKeyDelete": "Kunne ikke slette lisensnøkkel", + "licenseErrorKeyDeleteDescription": "Det oppstod en feil ved sletting av lisensnøkkel.", + "licenseKeyDeleted": "Lisensnøkkel slettet", + "licenseKeyDeletedDescription": "Lisensnøkkelen har blitt slettet.", + "licenseErrorKeyActivate": "Aktivering av lisensnøkkel feilet", + "licenseErrorKeyActivateDescription": "Det oppstod en feil under aktivering av lisensnøkkelen.", + "licenseAbout": "Om Lisensiering", + "communityEdition": "Fellesskapsutgave", + "licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.", + "licenseKeyActivated": "Lisensnøkkel aktivert", + "licenseKeyActivatedDescription": "Lisensnøkkelen har blitt vellykket aktivert.", + "licenseErrorKeyRecheck": "En feil oppsto under verifisering av lisensnøkler", + "licenseErrorKeyRecheckDescription": "Det oppstod en feil under verifisering av lisensnøkler.", + "licenseErrorKeyRechecked": "Lisensnøkler verifisert", + "licenseErrorKeyRecheckedDescription": "Alle lisensnøkler er verifisert", + "licenseActivateKey": "Aktiver lisensnøkkel", + "licenseActivateKeyDescription": "Skriv inn en lisensnøkkel for å aktivere den.", + "licenseActivate": "Aktiver lisens", + "licenseAgreement": "Ved å krysse av denne boksen bekrefter du at du har lest og godtar lisensvilkårene som tilsvarer nivået tilknyttet lisensnøkkelen din.", + "fossorialLicense": "Vis Fossorial kommersiell lisens og abonnementsvilkår", + "licenseMessageRemove": "Dette vil fjerne lisensnøkkelen og alle tilknyttede tillatelser gitt av den.", + "licenseMessageConfirm": "For å bekrefte, vennligst skriv inn lisensnøkkelen nedenfor.", + "licenseQuestionRemove": "Er du sikker på at du vil slette lisensnøkkelen {selectedKey} ?", + "licenseKeyDelete": "Slett Lisensnøkkel", + "licenseKeyDeleteConfirm": "Bekreft sletting av lisensnøkkel", + "licenseTitle": "Behandle lisensstatus", + "licenseTitleDescription": "Se og administrer lisensnøkler i systemet", + "licenseHost": "Vertslisens", + "licenseHostDescription": "Behandle hovedlisensnøkkelen for verten.", + "licensedNot": "Ikke lisensiert", + "hostId": "Verts-ID", + "licenseReckeckAll": "Verifiser alle nøkler", + "licenseSiteUsage": "Område Bruk", + "licenseSiteUsageDecsription": "Vis antall områder som bruker denne lisensen.", + "licenseNoSiteLimit": "Det er ingen grense på antall områder som bruker en ulisensiert vert.", + "licensePurchase": "Kjøp lisens", + "licensePurchaseSites": "Kjøp flere områder", + "licenseSitesUsedMax": "{usedSites} av {maxSites} områder brukt", + "licenseSitesUsed": "{count, plural, =0 {ingen områder} one {ett område} other {# områder}} i systemet.", + "licensePurchaseDescription": "Velg hvor mange områder du vil {selectedMode, select, license {kjøpe en lisens for. Du kan alltid legge til flere områder senere.} other {legge til din eksisterende lisens.}}", + "licenseFee": "Lisensavgift", + "licensePriceSite": "Pris per område", + "total": "Totalt", + "licenseContinuePayment": "Fortsett til betaling", + "pricingPage": "Pris oversikt", + "pricingPortal": "Se Kjøpsportal", + "licensePricingPage": "For de mest oppdaterte prisene og rabattene, vennligst besøk", + "invite": "Invitasjoner", + "inviteRegenerate": "Regenerer invitasjonen", + "inviteRegenerateDescription": "Tilbakekall tidligere invitasjon og opprette en ny", + "inviteRemove": "Fjern invitasjon", + "inviteRemoveError": "Mislyktes å fjerne invitasjon", + "inviteRemoveErrorDescription": "Det oppstod en feil under fjerning av invitasjonen.", + "inviteRemoved": "Invitasjon fjernet", + "inviteRemovedDescription": "Invitasjonen for {email} er fjernet.", + "inviteQuestionRemove": "Er du sikker på at du vil fjerne invitasjonen {email}?", + "inviteMessageRemove": "Når fjernet, vil denne invitasjonen ikke lenger være gyldig. Du kan alltid invitere brukeren på nytt senere.", + "inviteMessageConfirm": "For å bekrefte, vennligst tast inn invitasjonens e-postadresse nedenfor.", + "inviteQuestionRegenerate": "Er du sikker på at du vil generere invitasjonen på nytt for {email}? Dette vil ugyldiggjøre den forrige invitasjonen.", + "inviteRemoveConfirm": "Bekreft fjerning av invitasjon", + "inviteRegenerated": "Invitasjon fornyet", + "inviteSent": "En ny invitasjon er sendt til {email}.", + "inviteSentEmail": "Send e-postvarsel til brukeren", + "inviteGenerate": "En ny invitasjon er generert for {email}.", + "inviteDuplicateError": "Dupliser invitasjon", + "inviteDuplicateErrorDescription": "En invitasjon for denne brukeren eksisterer allerede.", + "inviteRateLimitError": "Forespørselsgrense overskredet", + "inviteRateLimitErrorDescription": "Du har overskredet grensen på 3 regenerasjoner per time. Prøv igjen senere.", + "inviteRegenerateError": "Kunne ikke regenerere invitasjon", + "inviteRegenerateErrorDescription": "Det oppsto en feil under regenerering av invitasjonen.", + "inviteValidityPeriod": "Gyldighetsperiode", + "inviteValidityPeriodSelect": "Velg gyldighetsperiode", + "inviteRegenerateMessage": "Invitasjonen er generert på nytt. Brukeren må gå til lenken nedenfor for å akseptere invitasjonen.", + "inviteRegenerateButton": "Regenerer", + "expiresAt": "Utløpstidspunkt", + "accessRoleUnknown": "Ukjent rolle", + "placeholder": "Plassholder", + "userErrorOrgRemove": "En feil oppsto under fjerning av bruker", + "userErrorOrgRemoveDescription": "Det oppstod en feil under fjerning av brukeren.", + "userOrgRemoved": "Bruker fjernet", + "userOrgRemovedDescription": "Brukeren {email} er fjernet fra organisasjonen.", + "userQuestionOrgRemove": "Er du sikker på at du vil fjerne {email} fra organisasjonen?", + "userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.", + "userMessageOrgConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", + "userRemoveOrgConfirm": "Bekreft fjerning av bruker", + "userRemoveOrg": "Fjern bruker fra organisasjon", + "users": "Brukere", + "accessRoleMember": "Medlem", + "accessRoleOwner": "Eier", + "userConfirmed": "Bekreftet", + "idpNameInternal": "Intern", + "emailInvalid": "Ugyldig e-postadresse", + "inviteValidityDuration": "Vennligst velg en varighet", + "accessRoleSelectPlease": "Vennligst velg en rolle", + "usernameRequired": "Brukernavn er påkrevd", + "idpSelectPlease": "Vennligst velg en identitetsleverandør", + "idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.", + "accessRoleErrorFetch": "En feil oppsto under henting av roller", + "accessRoleErrorFetchDescription": "En feil oppsto under henting av rollene", + "idpErrorFetch": "En feil oppsto under henting av identitetsleverandører", + "idpErrorFetchDescription": "En feil oppsto ved henting av identitetsleverandører", + "userErrorExists": "Bruker eksisterer allerede", + "userErrorExistsDescription": "Denne brukeren er allerede medlem av organisasjonen.", + "inviteError": "Kunne ikke invitere bruker", + "inviteErrorDescription": "En feil oppsto under invitering av brukeren", + "userInvited": "Bruker invitert", + "userInvitedDescription": "Brukeren er vellykket invitert.", + "userErrorCreate": "Kunne ikke opprette bruker", + "userErrorCreateDescription": "Det oppsto en feil under oppretting av brukeren", + "userCreated": "Bruker opprettet", + "userCreatedDescription": "Brukeren har blitt vellykket opprettet.", + "userTypeInternal": "Intern bruker", + "userTypeInternalDescription": "Inviter en bruker til å bli med i organisasjonen din direkte.", + "userTypeExternal": "Ekstern bruker", + "userTypeExternalDescription": "Opprett en bruker med en ekstern identitetsleverandør.", + "accessUserCreateDescription": "Følg stegene under for å opprette en ny bruker", + "userSeeAll": "Se alle brukere", + "userTypeTitle": "Brukertype", + "userTypeDescription": "Bestem hvordan du vil opprette brukeren", + "userSettings": "Brukerinformasjon", + "userSettingsDescription": "Skriv inn detaljene for den nye brukeren", + "inviteEmailSent": "Send invitasjonsepost til bruker", + "inviteValid": "Gyldig for", + "selectDuration": "Velg varighet", + "accessRoleSelect": "Velg rolle", + "inviteEmailSentDescription": "En e-post er sendt til brukeren med tilgangslenken nedenfor. De må åpne lenken for å akseptere invitasjonen.", + "inviteSentDescription": "Brukeren har blitt invitert. De må åpne lenken nedenfor for å godta invitasjonen.", + "inviteExpiresIn": "Invitasjonen utløper om {days, plural, one {en dag} other {# dager}}.", + "idpTitle": "Identitetsleverandør", + "idpSelect": "Velg identitetsleverandøren for den eksterne brukeren", + "idpNotConfigured": "Ingen identitetsleverandører er konfigurert. Vennligst konfigurer en identitetsleverandør før du oppretter eksterne brukere.", + "usernameUniq": "Dette må matche det unike brukernavnet som finnes i den valgte identitetsleverandøren.", + "emailOptional": "E-post (Valgfritt)", + "nameOptional": "Navn (valgfritt)", + "accessControls": "Tilgangskontroller", + "userDescription2": "Administrer innstillingene for denne brukeren", + "accessRoleErrorAdd": "Kunne ikke legge til bruker i rolle", + "accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.", + "userSaved": "Bruker lagret", + "userSavedDescription": "Brukeren har blitt oppdatert.", + "accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen", + "accessControlsSubmit": "Lagre tilgangskontroller", + "roles": "Roller", + "accessUsersRoles": "Administrer brukere og roller", + "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen din.", + "key": "Nøkkel", + "createdAt": "Opprettet", + "proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.", + "proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.", + "proxyEnableSSL": "Aktiver SSL (https)", + "targetErrorFetch": "Kunne ikke hente mål", + "targetErrorFetchDescription": "Det oppsto en feil under henting av mål", + "siteErrorFetch": "Klarte ikke å hente ressurs", + "siteErrorFetchDescription": "Det oppstod en feil under henting av ressurs", + "targetErrorDuplicate": "Dupliser mål", + "targetErrorDuplicateDescription": "Et mål med disse innstillingene finnes allerede", + "targetWireGuardErrorInvalidIp": "Ugyldig mål-IP", + "targetWireGuardErrorInvalidIpDescription": "Mål-IP må være i områdets undernett.", + "targetsUpdated": "Mål oppdatert", + "targetsUpdatedDescription": "Mål og innstillinger oppdatert vellykket", + "targetsErrorUpdate": "Feilet å oppdatere mål", + "targetsErrorUpdateDescription": "En feil oppsto under oppdatering av mål", + "targetTlsUpdate": "TLS-innstillinger oppdatert", + "targetTlsUpdateDescription": "Dine TLS-innstillinger er oppdatert", + "targetErrorTlsUpdate": "Feilet under oppdatering av TLS-innstillinger", + "targetErrorTlsUpdateDescription": "Det oppstod en feil under oppdatering av TLS-innstillinger", + "proxyUpdated": "Proxy-innstillinger oppdatert", + "proxyUpdatedDescription": "Proxy-innstillingene dine er oppdatert", + "proxyErrorUpdate": "En feil oppsto under oppdatering av proxyinnstillinger", + "proxyErrorUpdateDescription": "En feil oppsto under oppdatering av proxyinnstillinger", + "targetAddr": "IP / vertsnavn", + "targetPort": "Port", + "targetProtocol": "Protokoll", + "targetTlsSettings": "Sikker tilkoblings-konfigurasjon", + "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din", + "targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger", + "targetTlsSni": "TLS Servernavn (SNI)", + "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", + "targetTlsSubmit": "Lagre innstillinger", + "targets": "Målkonfigurasjon", + "targetsDescription": "Sett opp mål for å rute trafikk til tjenestene dine", + "targetStickySessions": "Aktiver klebrige sesjoner", + "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", + "methodSelect": "Velg metode", + "targetSubmit": "Legg til mål", + "targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.", + "targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.", + "targetsSubmit": "Lagre mål", + "proxyAdditional": "Ytterligere Proxy-innstillinger", + "proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger", + "proxyCustomHeader": "Tilpasset verts-header", + "proxyCustomHeaderDescription": "Verts-header som skal settes ved videresending av forespørsler. La stå tom for å bruke standardinnstillingen.", + "proxyAdditionalSubmit": "Lagre proxy-innstillinger", + "subnetMaskErrorInvalid": "Ugyldig subnettmaske. Må være mellom 0 og 32.", + "ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat", + "ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet", + "path": "Sti", + "ipAddressRange": "IP-område", + "rulesErrorFetch": "Klarte ikke å hente regler", + "rulesErrorFetchDescription": "Det oppsto en feil under henting av regler", + "rulesErrorDuplicate": "Duplisert regel", + "rulesErrorDuplicateDescription": "En regel med disse innstillingene finnes allerede", + "rulesErrorInvalidIpAddressRange": "Ugyldig CIDR", + "rulesErrorInvalidIpAddressRangeDescription": "Vennligst skriv inn en gyldig CIDR-verdi", + "rulesErrorInvalidUrl": "Ugyldig URL-sti", + "rulesErrorInvalidUrlDescription": "Skriv inn en gyldig verdi for URL-sti", + "rulesErrorInvalidIpAddress": "Ugyldig IP", + "rulesErrorInvalidIpAddressDescription": "Skriv inn en gyldig IP-adresse", + "rulesErrorUpdate": "Kunne ikke oppdatere regler", + "rulesErrorUpdateDescription": "Det oppsto en feil under oppdatering av regler", + "rulesUpdated": "Aktiver Regler", + "rulesUpdatedDescription": "Regelevalueringen har blitt oppdatert", + "rulesMatchIpAddressRangeDescription": "Angi en adresse i CIDR-format (f.eks., 103.21.244.0/22)", + "rulesMatchIpAddress": "Angi en IP-adresse (f.eks. 103.21.244.12)", + "rulesMatchUrl": "Skriv inn en URL-sti eller et mønster (f.eks. /api/v1/todos eller /api/v1/*)", + "rulesErrorInvalidPriority": "Ugyldig prioritet", + "rulesErrorInvalidPriorityDescription": "Vennligst skriv inn en gyldig prioritet", + "rulesErrorDuplicatePriority": "Dupliserte prioriteringer", + "rulesErrorDuplicatePriorityDescription": "Vennligst angi unike prioriteringer", + "ruleUpdated": "Regler oppdatert", + "ruleUpdatedDescription": "Reglene er oppdatert", + "ruleErrorUpdate": "Operasjon mislyktes", + "ruleErrorUpdateDescription": "En feil oppsto under lagringsoperasjonen", + "rulesPriority": "Prioritet", + "rulesAction": "Handling", + "rulesMatchType": "Trefftype", + "value": "Verdi", + "rulesAbout": "Om regler", + "rulesAboutDescription": "Regler lar deg kontrollere tilgang til din ressurs basert på et sett med kriterier. Du kan opprette regler for å tillate eller nekte tilgang basert på IP-adresse eller URL-sti.", + "rulesActions": "Handlinger", + "rulesActionAlwaysAllow": "Alltid Tillat: Omgå alle autentiserings metoder", + "rulesActionAlwaysDeny": "Alltid Nekt: Blokker alle forespørsler; ingen autentisering kan forsøkes", + "rulesMatchCriteria": "Samsvarende kriterier", + "rulesMatchCriteriaIpAddress": "Samsvar med en spesifikk IP-adresse", + "rulesMatchCriteriaIpAddressRange": "Samsvar et IP-adresseområde i CIDR-notasjon", + "rulesMatchCriteriaUrl": "Match en URL-sti eller et mønster", + "rulesEnable": "Aktiver regler", + "rulesEnableDescription": "Aktiver eller deaktiver regelvurdering for denne ressursen", + "rulesResource": "Konfigurasjon av ressursregler", + "rulesResourceDescription": "Konfigurere regler for tilgangskontroll til ressursen din", + "ruleSubmit": "Legg til regel", + "rulesNoOne": "Ingen regler. Legg til en regel ved å bruke skjemaet.", + "rulesOrder": "Regler evalueres etter prioritet i stigende rekkefølge.", + "rulesSubmit": "Lagre regler", + "resourceErrorCreate": "Feil under oppretting av ressurs", + "resourceErrorCreateDescription": "Det oppstod en feil under oppretting av ressursen", + "resourceErrorCreateMessage": "Feil ved oppretting av ressurs:", + "resourceErrorCreateMessageDescription": "En uventet feil oppstod", + "sitesErrorFetch": "Feil ved henting av områder", + "sitesErrorFetchDescription": "En feil oppstod ved henting av områdene", + "domainsErrorFetch": "Kunne ikke hente domener", + "domainsErrorFetchDescription": "Det oppsto en feil under henting av domenene", + "none": "Ingen", + "unknown": "Ukjent", + "resources": "Ressurser", + "resourcesDescription": "Ressurser er proxyer for applikasjoner som kjører på ditt private nettverk. Opprett en ressurs for enhver HTTP/HTTPS- eller rå TCP/UDP-tjeneste på ditt private nettverk. Hver ressurs må kobles til et område for å muliggjøre privat, sikker tilkobling gjennom en kryptert WireGuard-tunnel.", + "resourcesWireGuardConnect": "Sikker tilkobling med WireGuard-kryptering", + "resourcesMultipleAuthenticationMethods": "Konfigurer flere autentiseringsmetoder", + "resourcesUsersRolesAccess": "Bruker- og rollebasert tilgangskontroll", + "resourcesErrorUpdate": "Feilet å slå av/på ressurs", + "resourcesErrorUpdateDescription": "En feil oppstod under oppdatering av ressursen", + "access": "Tilgang", + "shareLink": "{resource} Del Lenke", + "resourceSelect": "Velg ressurs", + "shareLinks": "Del lenker", + "share": "Delbare lenker", + "shareDescription2": "Opprett delbare lenker til ressursene dine. Lenker gir midlertidig eller ubegrenset tilgang til ressursen din. Du kan konfigurere utløpsvarigheten for lenken når du oppretter den.", + "shareEasyCreate": "Enkelt å lage og dele", + "shareConfigurableExpirationDuration": "Konfigurerbar utløpsvarighet", + "shareSecureAndRevocable": "Sikker og tilbakekallbar", + "nameMin": "Navn må være minst {len} tegn.", + "nameMax": "Navn kan ikke være lengre enn {len} tegn.", + "sitesConfirmCopy": "Vennligst bekreft at du har kopiert konfigurasjonen.", + "unknownCommand": "Ukjent kommando", + "newtErrorFetchReleases": "Feilet å hente utgivelsesinfo: {err}", + "newtErrorFetchLatest": "Feil ved henting av siste utgivelse: {err}", + "newtEndpoint": "Newt endepunkt", + "newtId": "Newt-ID", + "newtSecretKey": "Newt hemmelig nøkkel", + "architecture": "Arkitektur", + "sites": "Områder", + "siteWgAnyClients": "Bruk en hvilken som helst WireGuard-klient for å koble til. Du må adressere dine interne ressurser ved å bruke peer-IP-en.", + "siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter", + "siteWgManualConfigurationRequired": "Manuell konfigurasjon påkrevd", + "userErrorNotAdminOrOwner": "Bruker er ikke administrator eller eier", + "pangolinSettings": "Innstillinger - Pangolin", + "accessRoleYour": "Din rolle:", + "accessRoleSelect2": "Velg en rolle", + "accessUserSelect": "Velg en bruker", + "otpEmailEnter": "Skriv inn én e-post", + "otpEmailEnterDescription": "Trykk enter for å legge til en e-post etter å ha tastet den inn i tekstfeltet.", + "otpEmailErrorInvalid": "Ugyldig e-postadresse. Jokertegnet (*) må være hele lokaldelen.", + "otpEmailSmtpRequired": "SMTP påkrevd", + "otpEmailSmtpRequiredDescription": "SMTP må være aktivert på serveren for å bruke engangspassord-autentisering.", + "otpEmailTitle": "Engangspassord", + "otpEmailTitleDescription": "Krev e-postbasert autentisering for ressurstilgang", + "otpEmailWhitelist": "E-post-hviteliste", + "otpEmailWhitelistList": "Hvitlistede e-poster", + "otpEmailWhitelistListDescription": "Kun brukere med disse e-postadressene vil ha tilgang til denne ressursen. De vil bli bedt om å skrive inn et engangspassord sendt til e-posten deres. Jokertegn (*@example.com) kan brukes for å tillate enhver e-postadresse fra et domene.", + "otpEmailWhitelistSave": "Lagre hvitliste", + "passwordAdd": "Legg til passord", + "passwordRemove": "Fjern passord", + "pincodeAdd": "Legg til PIN-kode", + "pincodeRemove": "Fjern PIN-kode", + "resourceAuthMethods": "Autentiseringsmetoder", + "resourceAuthMethodsDescriptions": "Tillat tilgang til ressursen via ytterligere autentiseringsmetoder", + "resourceAuthSettingsSave": "Lagret vellykket", + "resourceAuthSettingsSaveDescription": "Autentiseringsinnstillinger er lagret", + "resourceErrorAuthFetch": "Kunne ikke hente data", + "resourceErrorAuthFetchDescription": "Det oppstod en feil ved henting av data", + "resourceErrorPasswordRemove": "Feil ved fjerning av passord for ressurs", + "resourceErrorPasswordRemoveDescription": "Det oppstod en feil ved fjerning av ressurspassordet", + "resourceErrorPasswordSetup": "Feil ved innstilling av ressurspassord", + "resourceErrorPasswordSetupDescription": "Det oppstod en feil ved innstilling av ressurspassordet", + "resourceErrorPincodeRemove": "Feil ved fjerning av ressurs-PIN-koden", + "resourceErrorPincodeRemoveDescription": "Det oppstod en feil under fjerning av ressurs-pinkoden", + "resourceErrorPincodeSetup": "Feil ved innstilling av ressurs-PIN-kode", + "resourceErrorPincodeSetupDescription": "Det oppstod en feil under innstilling av ressursens PIN-kode", + "resourceErrorUsersRolesSave": "Klarte ikke å sette roller", + "resourceErrorUsersRolesSaveDescription": "En feil oppstod ved innstilling av rollene", + "resourceErrorWhitelistSave": "Feilet å lagre hvitliste", + "resourceErrorWhitelistSaveDescription": "Det oppstod en feil under lagring av hvitlisten", + "resourcePasswordSubmit": "Aktiver passordbeskyttelse", + "resourcePasswordProtection": "Passordbeskyttelse {status}", + "resourcePasswordRemove": "Ressurspassord fjernet", + "resourcePasswordRemoveDescription": "Fjerning av ressurspassordet var vellykket", + "resourcePasswordSetup": "Ressurspassord satt", + "resourcePasswordSetupDescription": "Ressurspassordet har blitt vellykket satt", + "resourcePasswordSetupTitle": "Angi passord", + "resourcePasswordSetupTitleDescription": "Sett et passord for å beskytte denne ressursen", + "resourcePincode": "PIN-kode", + "resourcePincodeSubmit": "Aktiver PIN-kodebeskyttelse", + "resourcePincodeProtection": "PIN-kodebeskyttelse {status}", + "resourcePincodeRemove": "Ressurs PIN-kode fjernet", + "resourcePincodeRemoveDescription": "Ressurspassordet ble fjernet", + "resourcePincodeSetup": "Ressurs PIN-kode satt", + "resourcePincodeSetupDescription": "Ressurs PIN-kode er satt vellykket", + "resourcePincodeSetupTitle": "Angi PIN-kode", + "resourcePincodeSetupTitleDescription": "Sett en pinkode for å beskytte denne ressursen", + "resourceRoleDescription": "Administratorer har alltid tilgang til denne ressursen.", + "resourceUsersRoles": "Brukere og Roller", + "resourceUsersRolesDescription": "Konfigurer hvilke brukere og roller som har tilgang til denne ressursen", + "resourceUsersRolesSubmit": "Lagre brukere og roller", + "resourceWhitelistSave": "Lagring vellykket", + "resourceWhitelistSaveDescription": "Hvitlisteinnstillinger er lagret", + "ssoUse": "Bruk plattform SSO", + "ssoUseDescription": "Eksisterende brukere trenger kun å logge på én gang for alle ressurser som har dette aktivert.", + "proxyErrorInvalidPort": "Ugyldig portnummer", + "subdomainErrorInvalid": "Ugyldig underdomene", + "domainErrorFetch": "Feil ved henting av domener", + "domainErrorFetchDescription": "Det oppstod en feil ved henting av domenene", + "resourceErrorUpdate": "Mislyktes å oppdatere ressurs", + "resourceErrorUpdateDescription": "Det oppstod en feil under oppdatering av ressursen", + "resourceUpdated": "Ressurs oppdatert", + "resourceUpdatedDescription": "Ressursen er oppdatert vellykket", + "resourceErrorTransfer": "Klarte ikke å overføre ressurs", + "resourceErrorTransferDescription": "En feil oppsto under overføring av ressursen", + "resourceTransferred": "Ressurs overført", + "resourceTransferredDescription": "Ressursen er overført vellykket.", + "resourceErrorToggle": "Feilet å veksle ressurs", + "resourceErrorToggleDescription": "Det oppstod en feil ved oppdatering av ressursen", + "resourceVisibilityTitle": "Synlighet", + "resourceVisibilityTitleDescription": "Fullstendig aktiver eller deaktiver ressursynlighet", + "resourceGeneral": "Generelle innstillinger", + "resourceGeneralDescription": "Konfigurer de generelle innstillingene for denne ressursen", + "resourceEnable": "Aktiver ressurs", + "resourceTransfer": "Overfør Ressurs", + "resourceTransferDescription": "Overfør denne ressursen til et annet område", + "resourceTransferSubmit": "Overfør ressurs", + "siteDestination": "Destinasjonsområde", + "searchSites": "Søk områder", + "accessRoleCreate": "Opprett rolle", + "accessRoleCreateDescription": "Opprett en ny rolle for å gruppere brukere og administrere deres tillatelser.", + "accessRoleCreateSubmit": "Opprett rolle", + "accessRoleCreated": "Rolle opprettet", + "accessRoleCreatedDescription": "Rollen er vellykket opprettet.", + "accessRoleErrorCreate": "Klarte ikke å opprette rolle", + "accessRoleErrorCreateDescription": "Det oppstod en feil under opprettelse av rollen.", + "accessRoleErrorNewRequired": "Ny rolle kreves", + "accessRoleErrorRemove": "Kunne ikke fjerne rolle", + "accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.", + "accessRoleName": "Rollenavn", + "accessRoleQuestionRemove": "Du er i ferd med å slette rollen {name}. Du kan ikke angre denne handlingen.", + "accessRoleRemove": "Fjern Rolle", + "accessRoleRemoveDescription": "Fjern en rolle fra organisasjonen", + "accessRoleRemoveSubmit": "Fjern Rolle", + "accessRoleRemoved": "Rolle fjernet", + "accessRoleRemovedDescription": "Rollen er vellykket fjernet.", + "accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.", + "manage": "Administrer", + "sitesNotFound": "Ingen områder funnet.", + "pangolinServerAdmin": "Server Admin - Pangolin", + "licenseTierProfessional": "Profesjonell lisens", + "licenseTierEnterprise": "Bedriftslisens", + "licenseTierCommercial": "Kommersiell lisens", + "licensed": "Lisensiert", + "yes": "Ja", + "no": "Nei", + "sitesAdditional": "Ytterligere områder", + "licenseKeys": "Lisensnøkler", + "sitestCountDecrease": "Reduser antall områder", + "sitestCountIncrease": "Øk antall områder", + "idpManage": "Administrer Identitetsleverandører", + "idpManageDescription": "Vis og administrer identitetsleverandører i systemet", + "idpDeletedDescription": "Identitetsleverandør slettet vellykket", + "idpOidc": "OAuth2/OIDC", + "idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren {name} permanent?", + "idpMessageRemove": "Dette vil fjerne identitetsleverandøren og alle tilhørende konfigurasjoner. Brukere som autentiserer seg via denne leverandøren vil ikke lenger kunne logge inn.", + "idpMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på identitetsleverandøren nedenfor.", + "idpConfirmDelete": "Bekreft Sletting av Identitetsleverandør", + "idpDelete": "Slett identitetsleverandør", + "idp": "Identitetsleverandører", + "idpSearch": "Søk identitetsleverandører...", + "idpAdd": "Legg til Identitetsleverandør", + "idpClientIdRequired": "Klient-ID er påkrevd.", + "idpClientSecretRequired": "Klienthemmelighet er påkrevd.", + "idpErrorAuthUrlInvalid": "Autentiserings-URL må være en gyldig URL.", + "idpErrorTokenUrlInvalid": "Token-URL må være en gyldig URL.", + "idpPathRequired": "Identifikatorbane er påkrevd.", + "idpScopeRequired": "Omfang kreves.", + "idpOidcDescription": "Konfigurer en OpenID Connect identitetsleverandør", + "idpCreatedDescription": "Identitetsleverandør opprettet vellykket.", + "idpCreate": "Opprett identitetsleverandør", + "idpCreateDescription": "Konfigurer en ny identitetsleverandør for brukerautentisering", + "idpSeeAll": "Se alle identitetsleverandører", + "idpSettingsDescription": "Konfigurer grunnleggende informasjon for din identitetsleverandør", + "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", + "idpAutoProvisionUsers": "Automatisk brukerklargjøring", + "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", + "licenseBadge": "Profesjonell", + "idpType": "Leverandørtype", + "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", + "idpOidcConfigure": "OAuth2/OIDC-konfigurasjon", + "idpOidcConfigureDescription": "Konfigurer OAuth2/OIDC-leverandørens endepunkter og legitimasjon", + "idpClientId": "Klient-ID", + "idpClientIdDescription": "OAuth2-klient-ID-en fra identitetsleverandøren din", + "idpClientSecret": "Klienthemmelighet", + "idpClientSecretDescription": "OAuth2-klienthemmeligheten fra din identitetsleverandør", + "idpAuthUrl": "Autorisasjons-URL", + "idpAuthUrlDescription": "OAuth2 autorisasjonsendepunkt URL", + "idpTokenUrl": "Token-URL", + "idpTokenUrlDescription": "OAuth2-tokenendepunkt-URL", + "idpOidcConfigureAlert": "Viktig informasjon", + "idpOidcConfigureAlertDescription": "Etter at du har opprettet identitetsleverandøren, må du konfigurere callback-URL-en i identitetsleverandørens innstillinger. Callback-URL-en blir oppgitt etter vellykket opprettelse.", + "idpToken": "Token-konfigurasjon", + "idpTokenDescription": "Konfigurer hvordan brukerinformasjon trekkes ut fra ID-tokenet", + "idpJmespathAbout": "Om JMESPath", + "idpJmespathAboutDescription": "Stiene nedenfor bruker JMESPath-syntaks for å hente ut verdier fra ID-tokenet.", + "idpJmespathAboutDescriptionLink": "Lær mer om JMESPath", + "idpJmespathLabel": "Identifikatorsti", + "idpJmespathLabelDescription": "Stien til brukeridentifikatoren i ID-tokenet", + "idpJmespathEmailPathOptional": "E-poststi (Valgfritt)", + "idpJmespathEmailPathOptionalDescription": "Stien til brukerens e-postadresse i ID-tokenet", + "idpJmespathNamePathOptional": "Navn Sti (Valgfritt)", + "idpJmespathNamePathOptionalDescription": "Stien til brukerens navn i ID-tokenet", + "idpOidcConfigureScopes": "Omfang", + "idpOidcConfigureScopesDescription": "Mellomromseparert liste over OAuth2-omfang å be om", + "idpSubmit": "Opprett identitetsleverandør", + "orgPolicies": "Organisasjonsretningslinjer", + "idpSettings": "{idpName} Innstillinger", + "idpCreateSettingsDescription": "Konfigurer innstillingene for din identitetsleverandør", + "roleMapping": "Rolletilordning", + "orgMapping": "Organisasjon Kartlegging", + "orgPoliciesSearch": "Søk i organisasjonens retningslinjer...", + "orgPoliciesAdd": "Legg til organisasjonspolicy", + "orgRequired": "Organisasjon er påkrevd", + "error": "Feil", + "success": "Suksess", + "orgPolicyAddedDescription": "Policy vellykket lagt til", + "orgPolicyUpdatedDescription": "Policyen er vellykket oppdatert", + "orgPolicyDeletedDescription": "Policy slettet vellykket", + "defaultMappingsUpdatedDescription": "Standardtilordninger oppdatert vellykket", + "orgPoliciesAbout": "Om organisasjonens retningslinjer", + "orgPoliciesAboutDescription": "Organisasjonspolicyer brukes til å kontrollere tilgang til organisasjoner basert på brukerens ID-token. Du kan spesifisere JMESPath-uttrykk for å trekke ut rolle- og organisasjonsinformasjon fra ID-tokenet.", + "orgPoliciesAboutDescriptionLink": "Se dokumentasjon, for mer informasjon.", + "defaultMappingsOptional": "Standard Tilordninger (Valgfritt)", + "defaultMappingsOptionalDescription": "Standardtilordningene brukes når det ikke er definert en organisasjonspolicy for en organisasjon. Du kan spesifisere standard rolle- og organisasjonstilordninger som det kan falles tilbake på her.", + "defaultMappingsRole": "Standard rolletilordning", + "defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.", + "defaultMappingsOrg": "Standard organisasjonstilordning", + "defaultMappingsOrgDescription": "Dette uttrykket må returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.", + "defaultMappingsSubmit": "Lagre standard tilordninger", + "orgPoliciesEdit": "Rediger Organisasjonspolicy", + "org": "Organisasjon", + "orgSelect": "Velg organisasjon", + "orgSearch": "Søk organisasjon", + "orgNotFound": "Ingen organisasjon funnet.", + "roleMappingPathOptional": "Rollekartleggingssti (Valgfritt)", + "orgMappingPathOptional": "Organisasjonstilordningssti (Valgfritt)", + "orgPolicyUpdate": "Oppdater policy", + "orgPolicyAdd": "Legg til policy", + "orgPolicyConfig": "Konfigurer tilgang for en organisasjon", + "idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert", + "redirectUrl": "Omdirigerings-URL", + "redirectUrlAbout": "Om omdirigerings-URL", + "redirectUrlAboutDescription": "Dette er URL-en som brukere vil bli omdirigert til etter autentisering. Du må konfigurere denne URL-en i innstillingene for identitetsleverandøren din.", + "pangolinAuth": "Autentisering - Pangolin", + "verificationCodeLengthRequirements": "Din verifiseringskode må være 8 tegn.", + "errorOccurred": "Det oppstod en feil", + "emailErrorVerify": "Kunne ikke verifisere e-post:", + "emailVerified": "E-posten er bekreftet! Omdirigerer deg...", + "verificationCodeErrorResend": "Kunne ikke sende bekreftelseskode på nytt:", + "verificationCodeResend": "Bekreftelseskode sendt på nytt", + "verificationCodeResendDescription": "Vi har sendt en ny bekreftelseskode til e-postadressen din. Vennligst sjekk innboksen din.", + "emailVerify": "Verifiser e-post", + "emailVerifyDescription": "Skriv inn bekreftelseskoden sendt til e-postadressen din.", + "verificationCode": "Verifiseringskode", + "verificationCodeEmailSent": "Vi har sendt en bekreftelseskode til e-postadressen din.", + "submit": "Send inn", + "emailVerifyResendProgress": "Sender på nytt...", + "emailVerifyResend": "Har du ikke mottatt en kode? Klikk her for å sende på nytt", + "passwordNotMatch": "Passordene stemmer ikke", + "signupError": "Det oppsto en feil ved registrering", + "pangolinLogoAlt": "Pangolin Logo", + "inviteAlready": "Ser ut til at du har blitt invitert!", + "inviteAlreadyDescription": "For å godta invitasjonen, må du logge inn eller opprette en konto.", + "signupQuestion": "Har du allerede en konto?", + "login": "Logg inn", + "resourceNotFound": "Ressurs ikke funnet", + "resourceNotFoundDescription": "Ressursen du prøver å få tilgang til eksisterer ikke.", + "pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer", + "pincodeRequirementsChars": "PIN må kun inneholde tall", + "passwordRequirementsLength": "Passord må være minst 1 tegn langt", + "otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.", + "otpEmailSent": "OTP sendt", + "otpEmailSentDescription": "En OTP er sendt til din e-post", + "otpEmailErrorAuthenticate": "Mislyktes å autentisere med e-post", + "pincodeErrorAuthenticate": "Kunne ikke autentisere med pinkode", + "passwordErrorAuthenticate": "Kunne ikke autentisere med passord", + "poweredBy": "Drevet av", + "authenticationRequired": "Autentisering påkrevd", + "authenticationMethodChoose": "Velg din foretrukne metode for å få tilgang til {name}", + "authenticationRequest": "Du må autentisere deg for å få tilgang til {name}", + "user": "Bruker", + "pincodeInput": "6-sifret PIN-kode", + "pincodeSubmit": "Logg inn med PIN", + "passwordSubmit": "Logg inn med passord", + "otpEmailDescription": "En engangskode vil bli sendt til denne e-posten.", + "otpEmailSend": "Send engangskode", + "otpEmail": "Engangspassord (OTP)", + "otpEmailSubmit": "Send inn OTP", + "backToEmail": "Tilbake til E-post", + "noSupportKey": "Serveren kjører uten en supporterlisens. Vurder å støtte prosjektet!", + "accessDenied": "Tilgang nektet", + "accessDeniedDescription": "Du har ikke tilgang til denne ressursen. Hvis dette er en feil, vennligst kontakt administratoren.", + "accessTokenError": "Feil ved sjekk av tilgangstoken", + "accessGranted": "Tilgang gitt", + "accessUrlInvalid": "Ugyldig tilgangs-URL", + "accessGrantedDescription": "Du har fått tilgang til denne ressursen. Omdirigerer deg...", + "accessUrlInvalidDescription": "Denne delings-URL-en er ugyldig. Vennligst kontakt ressurseieren for en ny URL.", + "tokenInvalid": "Ugyldig token", + "pincodeInvalid": "Ugyldig kode", + "passwordErrorRequestReset": "Forespørsel om tilbakestilling mislyktes", + "passwordErrorReset": "Klarte ikke å tilbakestille passord:", + "passwordResetSuccess": "Passordet er tilbakestilt! Går tilbake til innlogging...", + "passwordReset": "Tilbakestill passord", + "passwordResetDescription": "Følg stegene for å tilbakestille passordet ditt", + "passwordResetSent": "Vi sender en kode for tilbakestilling av passord til denne e-postadressen.", + "passwordResetCode": "Tilbakestillingskode", + "passwordResetCodeDescription": "Sjekk e-posten din for tilbakestillingskoden.", + "passwordNew": "Nytt passord", + "passwordNewConfirm": "Bekreft nytt passord", + "pincodeAuth": "Autentiseringskode", + "pincodeSubmit2": "Send inn kode", + "passwordResetSubmit": "Be om tilbakestilling", + "passwordBack": "Tilbake til passord", + "loginBack": "Gå tilbake til innlogging", + "signup": "Registrer deg", + "loginStart": "Logg inn for å komme i gang", + "idpOidcTokenValidating": "Validerer OIDC-token", + "idpOidcTokenResponse": "Valider OIDC-tokensvar", + "idpErrorOidcTokenValidating": "Feil ved validering av OIDC-token", + "idpConnectingTo": "Kobler til {name}", + "idpConnectingToDescription": "Validerer identiteten din", + "idpConnectingToProcess": "Kobler til...", + "idpConnectingToFinished": "Tilkoblet", + "idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.", + "idpErrorNotFound": "IdP ikke funnet", + "inviteInvalid": "Ugyldig invitasjon", + "inviteInvalidDescription": "Invitasjonslenken er ugyldig.", + "inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren", + "inviteErrorUserNotExists": "Brukeren eksisterer ikke. Vennligst opprett en konto først.", + "inviteErrorLoginRequired": "Du må være logget inn for å godta en invitasjon", + "inviteErrorExpired": "Invitasjonen kan ha utløpt", + "inviteErrorRevoked": "Invitasjonen kan ha blitt trukket tilbake", + "inviteErrorTypo": "Det kan være en skrivefeil i invitasjonslenken", + "pangolinSetup": "Oppsett - Pangolin", + "orgNameRequired": "Organisasjonsnavn er påkrevd", + "orgIdRequired": "Organisasjons-ID er påkrevd", + "orgErrorCreate": "En feil oppstod under oppretting av organisasjon", + "pageNotFound": "Siden ble ikke funnet", + "pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.", + "overview": "Oversikt", + "home": "Hjem", + "accessControl": "Tilgangskontroll", + "settings": "Innstillinger", + "usersAll": "Alle brukere", + "license": "Lisens", + "pangolinDashboard": "Dashbord - Pangolin", + "noResults": "Ingen resultater funnet.", + "terabytes": "{count} TB", + "gigabytes": "{count} GB", + "megabytes": "{count} MB", + "tagsEntered": "Inntastede tagger", + "tagsEnteredDescription": "Dette er taggene du har tastet inn.", + "tagsWarnCannotBeLessThanZero": "maxTags og minTags kan ikke være mindre enn 0", + "tagsWarnNotAllowedAutocompleteOptions": "Tagg ikke tillatt i henhold til autofullfør-alternativer", + "tagsWarnInvalid": "Ugyldig tagg i henhold til validateTag", + "tagWarnTooShort": "Tagg {tagText} er for kort", + "tagWarnTooLong": "Tagg {tagText} er for lang", + "tagsWarnReachedMaxNumber": "Maksimalt antall tillatte tagger er nådd", + "tagWarnDuplicate": "Duplisert tagg {tagText} ble ikke lagt til", + "supportKeyInvalid": "Ugyldig nøkkel", + "supportKeyInvalidDescription": "Din supporternøkkel er ugyldig.", + "supportKeyValid": "Gyldig nøkkel", + "supportKeyValidDescription": "Din supporternøkkel er validert. Takk for din støtte!", + "supportKeyErrorValidationDescription": "Klarte ikke å validere supporternøkkel.", + "supportKey": "Støtt utviklingen og adopter en Pangolin!", + "supportKeyDescription": "Kjøp en supporternøkkel for å hjelpe oss med å fortsette utviklingen av Pangolin for fellesskapet. Ditt bidrag lar oss bruke mer tid på å vedlikeholde og legge til nye funksjoner i applikasjonen for alle. Vi vil aldri bruke dette til å legge funksjoner bak en betalingsmur. Dette er atskilt fra enhver kommersiell utgave.", + "supportKeyPet": "Du vil også få adoptere og møte din helt egen kjæledyr-Pangolin!", + "supportKeyPurchase": "Betalinger behandles via GitHub. Etterpå kan du hente nøkkelen din på", + "supportKeyPurchaseLink": "vår nettside", + "supportKeyPurchase2": "og løse den inn her.", + "supportKeyLearnMore": "Lær mer.", + "supportKeyOptions": "Vennligst velg det alternativet som passer deg best.", + "supportKetOptionFull": "Full støttespiller", + "forWholeServer": "For hele serveren", + "lifetimePurchase": "Livstidskjøp", + "supporterStatus": "Supporterstatus", + "buy": "Kjøp", + "supportKeyOptionLimited": "Begrenset støttespiller", + "forFiveUsers": "For 5 eller færre brukere", + "supportKeyRedeem": "Løs inn supporternøkkel", + "supportKeyHideSevenDays": "Skjul i 7 dager", + "supportKeyEnter": "Skriv inn supporternøkkel", + "supportKeyEnterDescription": "Møt din helt egen kjæledyr-Pangolin!", + "githubUsername": "GitHub-brukernavn", + "supportKeyInput": "Supporternøkkel", + "supportKeyBuy": "Kjøp supporternøkkel", + "logoutError": "Feil ved utlogging", + "signingAs": "Logget inn som", + "serverAdmin": "Serveradministrator", + "otpEnable": "Aktiver tofaktor", + "otpDisable": "Deaktiver tofaktor", + "logout": "Logg ut", + "licenseTierProfessionalRequired": "Profesjonell utgave påkrevd", + "licenseTierProfessionalRequiredDescription": "Denne funksjonen er kun tilgjengelig i den profesjonelle utgaven.", + "actionGetOrg": "Hent organisasjon", + "actionUpdateOrg": "Oppdater organisasjon", + "actionUpdateUser": "Oppdater bruker", + "actionGetUser": "Hent bruker", + "actionGetOrgUser": "Hent organisasjonsbruker", + "actionListOrgDomains": "List opp organisasjonsdomener", + "actionCreateSite": "Opprett område", + "actionDeleteSite": "Slett område", + "actionGetSite": "Hent område", + "actionListSites": "List opp områder", + "actionUpdateSite": "Oppdater område", + "actionListSiteRoles": "List opp tillatte områderoller", + "actionCreateResource": "Opprett ressurs", + "actionDeleteResource": "Slett ressurs", + "actionGetResource": "Hent ressurs", + "actionListResource": "List opp ressurser", + "actionUpdateResource": "Oppdater ressurs", + "actionListResourceUsers": "List opp ressursbrukere", + "actionSetResourceUsers": "Angi ressursbrukere", + "actionSetAllowedResourceRoles": "Angi tillatte ressursroller", + "actionListAllowedResourceRoles": "List opp tillatte ressursroller", + "actionSetResourcePassword": "Angi ressurspassord", + "actionSetResourcePincode": "Angi ressurspinkode", + "actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs", + "actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs", + "actionCreateTarget": "Opprett mål", + "actionDeleteTarget": "Slett mål", + "actionGetTarget": "Hent mål", + "actionListTargets": "List opp mål", + "actionUpdateTarget": "Oppdater mål", + "actionCreateRole": "Opprett rolle", + "actionDeleteRole": "Slett rolle", + "actionGetRole": "Hent rolle", + "actionListRole": "List opp roller", + "actionUpdateRole": "Oppdater rolle", + "actionListAllowedRoleResources": "List opp tillatte rolleressurser", + "actionInviteUser": "Inviter bruker", + "actionRemoveUser": "Fjern bruker", + "actionListUsers": "List opp brukere", + "actionAddUserRole": "Legg til brukerrolle", + "actionGenerateAccessToken": "Generer tilgangstoken", + "actionDeleteAccessToken": "Slett tilgangstoken", + "actionListAccessTokens": "List opp tilgangstokener", + "actionCreateResourceRule": "Opprett ressursregel", + "actionDeleteResourceRule": "Slett ressursregel", + "actionListResourceRules": "List opp ressursregler", + "actionUpdateResourceRule": "Oppdater ressursregel", + "actionListOrgs": "List opp organisasjoner", + "actionCheckOrgId": "Sjekk ID", + "actionCreateOrg": "Opprett organisasjon", + "actionDeleteOrg": "Slett organisasjon", + "actionListApiKeys": "List opp API-nøkler", + "actionListApiKeyActions": "List opp API-nøkkelhandlinger", + "actionSetApiKeyActions": "Angi tillatte handlinger for API-nøkkel", + "actionCreateApiKey": "Opprett API-nøkkel", + "actionDeleteApiKey": "Slett API-nøkkel", + "actionCreateIdp": "Opprett IDP", + "actionUpdateIdp": "Oppdater IDP", + "actionDeleteIdp": "Slett IDP", + "actionListIdps": "List opp IDP-er", + "actionGetIdp": "Hent IDP", + "actionCreateIdpOrg": "Opprett IDP-organisasjonspolicy", + "actionDeleteIdpOrg": "Slett IDP-organisasjonspolicy", + "actionListIdpOrgs": "List opp IDP-organisasjoner", + "actionUpdateIdpOrg": "Oppdater IDP-organisasjon", + "actionCreateClient": "Opprett Klient", + "actionDeleteClient": "Slett klient", + "actionUpdateClient": "Oppdater klient", + "actionListClients": "List klienter", + "actionGetClient": "Hent klient", + "noneSelected": "Ingen valgt", + "orgNotFound2": "Ingen organisasjoner funnet.", + "searchProgress": "Søker...", + "create": "Opprett", + "orgs": "Organisasjoner", + "loginError": "En feil oppstod under innlogging", + "passwordForgot": "Glemt passordet ditt?", + "otpAuth": "Tofaktorautentisering", + "otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.", + "otpAuthSubmit": "Send inn kode", + "idpContinue": "Eller fortsett med", + "otpAuthBack": "Tilbake til innlogging", + "navbar": "Navigasjonsmeny", + "navbarDescription": "Hovednavigasjonsmeny for applikasjonen", + "navbarDocsLink": "Dokumentasjon", + "commercialEdition": "Kommersiell utgave", + "otpErrorEnable": "Kunne ikke aktivere 2FA", + "otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA", + "otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode", + "otpSetupCheckCodeRetry": "Ugyldig kode. Vennligst prøv igjen.", + "otpSetup": "Aktiver tofaktorautentisering", + "otpSetupDescription": "Sikre kontoen din med et ekstra lag med beskyttelse", + "otpSetupScanQr": "Skann denne QR-koden med autentiseringsappen din eller skriv inn den hemmelige nøkkelen manuelt:", + "otpSetupSecretCode": "Autentiseringskode", + "otpSetupSuccess": "Tofaktorautentisering aktivert", + "otpSetupSuccessStoreBackupCodes": "Kontoen din er nå sikrere. Ikke glem å lagre reservekodene dine.", + "otpErrorDisable": "Kunne ikke deaktivere 2FA", + "otpErrorDisableDescription": "En feil oppstod under deaktivering av 2FA", + "otpRemove": "Deaktiver tofaktorautentisering", + "otpRemoveDescription": "Deaktiver tofaktorautentisering for kontoen din", + "otpRemoveSuccess": "Tofaktorautentisering deaktivert", + "otpRemoveSuccessMessage": "Tofaktorautentisering er deaktivert for kontoen din. Du kan aktivere den igjen når som helst.", + "otpRemoveSubmit": "Deaktiver 2FA", + "paginator": "Side {current} av {last}", + "paginatorToFirst": "Gå til første side", + "paginatorToPrevious": "Gå til forrige side", + "paginatorToNext": "Gå til neste side", + "paginatorToLast": "Gå til siste side", + "copyText": "Kopier tekst", + "copyTextFailed": "Klarte ikke å kopiere tekst: ", + "copyTextClipboard": "Kopier til utklippstavle", + "inviteErrorInvalidConfirmation": "Ugyldig bekreftelse", + "passwordRequired": "Passord er påkrevd", + "allowAll": "Tillat alle", + "permissionsAllowAll": "Tillat alle rettigheter", + "githubUsernameRequired": "GitHub-brukernavn er påkrevd", + "supportKeyRequired": "supporternøkkel er påkrevd", + "passwordRequirementsChars": "Passordet må være minst 8 tegn", + "language": "Språk", + "verificationCodeRequired": "Kode er påkrevd", + "userErrorNoUpdate": "Ingen bruker å oppdatere", + "siteErrorNoUpdate": "Ingen område å oppdatere", + "resourceErrorNoUpdate": "Ingen ressurs å oppdatere", + "authErrorNoUpdate": "Ingen autentiseringsinfo å oppdatere", + "orgErrorNoUpdate": "Ingen organisasjon å oppdatere", + "orgErrorNoProvided": "Ingen organisasjon angitt", + "apiKeysErrorNoUpdate": "Ingen API-nøkkel å oppdatere", + "sidebarOverview": "Oversikt", + "sidebarHome": "Hjem", + "sidebarSites": "Områder", + "sidebarResources": "Ressurser", + "sidebarAccessControl": "Tilgangskontroll", + "sidebarUsers": "Brukere", + "sidebarInvitations": "Invitasjoner", + "sidebarRoles": "Roller", + "sidebarShareableLinks": "Delbare lenker", + "sidebarApiKeys": "API-nøkler", + "sidebarSettings": "Innstillinger", + "sidebarAllUsers": "Alle brukere", + "sidebarIdentityProviders": "Identitetsleverandører", + "sidebarLicense": "Lisens", + "sidebarClients": "Klienter (Beta)", + "sidebarDomains": "Domener", + "enableDockerSocket": "Aktiver Docker Socket", + "enableDockerSocketDescription": "Aktiver Docker Socket-oppdagelse for å fylle ut containerinformasjon. Socket-stien må oppgis til Newt.", + "enableDockerSocketLink": "Lær mer", + "viewDockerContainers": "Vis Docker-containere", + "containersIn": "Containere i {siteName}", + "selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.", + "containerName": "Navn", + "containerImage": "Bilde", + "containerState": "Tilstand", + "containerNetworks": "Nettverk", + "containerHostnameIp": "Vertsnavn/IP", + "containerLabels": "Etiketter", + "containerLabelsCount": "{count, plural, one {en etikett} other {# etiketter}}", + "containerLabelsTitle": "Containeretiketter", + "containerLabelEmpty": "", + "containerPorts": "Porter", + "containerPortsMore": "+{count} til", + "containerActions": "Handlinger", + "select": "Velg", + "noContainersMatchingFilters": "Ingen containere funnet som matcher de nåværende filtrene.", + "showContainersWithoutPorts": "Vis containere uten porter", + "showStoppedContainers": "Vis stoppede containere", + "noContainersFound": "Ingen containere funnet. Sørg for at Docker-containere kjører.", + "searchContainersPlaceholder": "Søk blant {count} containere...", + "searchResultsCount": "{count, plural, one {ett resultat} other {# resultater}}", + "filters": "Filtre", + "filterOptions": "Filteralternativer", + "filterPorts": "Porter", + "filterStopped": "Stoppet", + "clearAllFilters": "Fjern alle filtre", + "columns": "Kolonner", + "toggleColumns": "Vis/skjul kolonner", + "refreshContainersList": "Oppdater containerliste", + "searching": "Søker...", + "noContainersFoundMatching": "Ingen containere funnet som matcher \"{filter}\".", + "light": "lys", + "dark": "mørk", + "system": "system", + "theme": "Tema", + "subnetRequired": "Subnett er påkrevd", + "initialSetupTitle": "Førstegangsoppsett av server", + "initialSetupDescription": "Opprett den første serveradministratorkontoen. Det kan bare finnes én serveradministrator. Du kan alltid endre denne påloggingsinformasjonen senere.", + "createAdminAccount": "Opprett administratorkonto", + "setupErrorCreateAdmin": "En feil oppstod under opprettelsen av serveradministratorkontoen.", + "certificateStatus": "Sertifikatstatus", + "loading": "Laster inn", + "restart": "Start på nytt", + "domains": "Domener", + "domainsDescription": "Administrer domener for organisasjonen din", + "domainsSearch": "Søk i domener...", + "domainAdd": "Legg til domene", + "domainAddDescription": "Registrer et nytt domene hos organisasjonen din", + "domainCreate": "Opprett domene", + "domainCreatedDescription": "Domene ble opprettet", + "domainDeletedDescription": "Domene ble slettet", + "domainQuestionRemove": "Er du sikker på at du vil fjerne domenet {domain} fra kontoen din?", + "domainMessageRemove": "Når domenet er fjernet, vil det ikke lenger være knyttet til kontoen din.", + "domainMessageConfirm": "For å bekrefte, vennligst skriv inn domenenavnet nedenfor.", + "domainConfirmDelete": "Bekreft sletting av domene", + "domainDelete": "Slett domene", + "domain": "Domene", + "selectDomainTypeNsName": "Domenedelegering (NS)", + "selectDomainTypeNsDescription": "Dette domenet og alle dets underdomener. Bruk dette når du vil kontrollere en hel domenesone.", + "selectDomainTypeCnameName": "Enkelt domene (CNAME)", + "selectDomainTypeCnameDescription": "Bare dette spesifikke domenet. Bruk dette for individuelle underdomener eller spesifikke domeneoppføringer.", + "selectDomainTypeWildcardName": "Wildcard-domene", + "selectDomainTypeWildcardDescription": "Dette domenet og dets underdomener.", + "domainDelegation": "Enkelt domene", + "selectType": "Velg en type", + "actions": "Handlinger", + "refresh": "Oppdater", + "refreshError": "Klarte ikke å oppdatere data", + "verified": "Verifisert", + "pending": "Venter", + "sidebarBilling": "Fakturering", + "billing": "Fakturering", + "orgBillingDescription": "Administrer faktureringsinformasjon og abonnementer", + "github": "GitHub", + "pangolinHosted": "Driftet av Pangolin", + "fossorial": "Fossorial", + "completeAccountSetup": "Fullfør kontooppsett", + "completeAccountSetupDescription": "Angi passordet ditt for å komme i gang", + "accountSetupSent": "Vi sender en oppsettskode for kontoen til denne e-postadressen.", + "accountSetupCode": "Oppsettskode", + "accountSetupCodeDescription": "Sjekk e-posten din for oppsettskoden.", + "passwordCreate": "Opprett passord", + "passwordCreateConfirm": "Bekreft passord", + "accountSetupSubmit": "Send oppsettskode", + "completeSetup": "Fullfør oppsett", + "accountSetupSuccess": "Kontooppsett fullført! Velkommen til Pangolin!", + "documentation": "Dokumentasjon", + "saveAllSettings": "Lagre alle innstillinger", + "settingsUpdated": "Innstillinger oppdatert", + "settingsUpdatedDescription": "Alle innstillinger er oppdatert", + "settingsErrorUpdate": "Klarte ikke å oppdatere innstillinger", + "settingsErrorUpdateDescription": "En feil oppstod under oppdatering av innstillinger", + "sidebarCollapse": "Skjul", + "sidebarExpand": "Utvid", + "newtUpdateAvailable": "Oppdatering tilgjengelig", + "newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.", + "domainPickerEnterDomain": "Domene", + "domainPickerPlaceholder": "minapp.eksempel.com, api.v1.mittdomene.com, eller bare minapp", + "domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.", + "domainPickerDescriptionSaas": "Skriv inn et fullt domene, underdomene eller bare et navn for å se tilgjengelige alternativer", + "domainPickerTabAll": "Alle", + "domainPickerTabOrganization": "Organisasjon", + "domainPickerTabProvided": "Levert", + "domainPickerSortAsc": "A-Å", + "domainPickerSortDesc": "Å-A", + "domainPickerCheckingAvailability": "Sjekker tilgjengelighet...", + "domainPickerNoMatchingDomains": "Ingen samsvarende domener funnet. Prøv et annet domene eller sjekk organisasjonens domeneinnstillinger.", + "domainPickerOrganizationDomains": "Organisasjonsdomener", + "domainPickerProvidedDomains": "Leverte domener", + "domainPickerSubdomain": "Underdomene: {subdomain}", + "domainPickerNamespace": "Navnerom: {namespace}", + "domainPickerShowMore": "Vis mer", + "domainNotFound": "Domene ikke funnet", + "domainNotFoundDescription": "Denne ressursen er deaktivert fordi domenet ikke lenger eksisterer i systemet vårt. Vennligst angi et nytt domene for denne ressursen.", + "failed": "Mislyktes", + "createNewOrgDescription": "Opprett en ny organisasjon", + "organization": "Organisasjon", + "port": "Port", + "securityKeyManage": "Administrer sikkerhetsnøkler", + "securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering", + "securityKeyRegister": "Registrer ny sikkerhetsnøkkel", + "securityKeyList": "Dine sikkerhetsnøkler", + "securityKeyNone": "Ingen sikkerhetsnøkler er registrert enda", + "securityKeyNameRequired": "Navn er påkrevd", + "securityKeyRemove": "Fjern", + "securityKeyLastUsed": "Sist brukt: {date}", + "securityKeyNameLabel": "Navn på sikkerhetsnøkkel", + "securityKeyRegisterSuccess": "Sikkerhetsnøkkel registrert", + "securityKeyRegisterError": "Klarte ikke å registrere sikkerhetsnøkkel", + "securityKeyRemoveSuccess": "Sikkerhetsnøkkel fjernet", + "securityKeyRemoveError": "Klarte ikke å fjerne sikkerhetsnøkkel", + "securityKeyLoadError": "Klarte ikke å laste inn sikkerhetsnøkler", + "securityKeyLogin": "Fortsett med sikkerhetsnøkkel", + "securityKeyAuthError": "Klarte ikke å autentisere med sikkerhetsnøkkel", + "securityKeyRecommendation": "Registrer en reservesikkerhetsnøkkel på en annen enhet for å sikre at du alltid har tilgang til kontoen din.", + "registering": "Registrerer...", + "securityKeyPrompt": "Vennligst verifiser identiteten din med sikkerhetsnøkkelen. Sørg for at sikkerhetsnøkkelen er koblet til og klar.", + "securityKeyBrowserNotSupported": "Nettleseren din støtter ikke sikkerhetsnøkler. Vennligst bruk en moderne nettleser som Chrome, Firefox eller Safari.", + "securityKeyPermissionDenied": "Vennligst tillat tilgang til sikkerhetsnøkkelen din for å fortsette innloggingen.", + "securityKeyRemovedTooQuickly": "Vennligst hold sikkerhetsnøkkelen tilkoblet til innloggingsprosessen er fullført.", + "securityKeyNotSupported": "Sikkerhetsnøkkelen din er kanskje ikke kompatibel. Vennligst prøv en annen sikkerhetsnøkkel.", + "securityKeyUnknownError": "Det oppstod et problem med å bruke sikkerhetsnøkkelen din. Vennligst prøv igjen.", + "twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.", + "twoFactor": "Tofaktorautentisering", + "adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.", + "continueToApplication": "Fortsett til applikasjonen", + "securityKeyAdd": "Legg til sikkerhetsnøkkel", + "securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel", + "securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den", + "securityKeyTwoFactorRequired": "Tofaktorautentisering påkrevd", + "securityKeyTwoFactorDescription": "Vennligst skriv inn koden for tofaktorautentisering for å registrere sikkerhetsnøkkelen", + "securityKeyTwoFactorRemoveDescription": "Vennligst skriv inn koden for tofaktorautentisering for å fjerne sikkerhetsnøkkelen", + "securityKeyTwoFactorCode": "Tofaktorkode", + "securityKeyRemoveTitle": "Fjern sikkerhetsnøkkel", + "securityKeyRemoveDescription": "Skriv inn passordet ditt for å fjerne sikkerhetsnøkkelen \"{name}\"", + "securityKeyNoKeysRegistered": "Ingen sikkerhetsnøkler registrert", + "securityKeyNoKeysDescription": "Legg til en sikkerhetsnøkkel for å øke sikkerheten på kontoen din", + "createDomainRequired": "Domene er påkrevd", + "createDomainAddDnsRecords": "Legg til DNS-oppføringer", + "createDomainAddDnsRecordsDescription": "Legg til følgende DNS-oppføringer hos din domeneleverandør for å fullføre oppsettet.", + "createDomainNsRecords": "NS-oppføringer", + "createDomainRecord": "Oppføring", + "createDomainType": "Type:", + "createDomainName": "Navn:", + "createDomainValue": "Verdi:", + "createDomainCnameRecords": "CNAME-oppføringer", + "createDomainARecords": "A-oppføringer", + "createDomainRecordNumber": "Oppføring {number}", + "createDomainTxtRecords": "TXT-oppføringer", + "createDomainSaveTheseRecords": "Lagre disse oppføringene", + "createDomainSaveTheseRecordsDescription": "Sørg for å lagre disse DNS-oppføringene, da du ikke vil se dem igjen.", + "createDomainDnsPropagation": "DNS-propagering", + "createDomainDnsPropagationDescription": "DNS-endringer kan ta litt tid å propagere over internett. Dette kan ta fra noen få minutter til 48 timer, avhengig av din DNS-leverandør og TTL-innstillinger.", + "resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser", + "resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser", + "signUpTerms": { + "IAgreeToThe": "Jeg godtar", + "termsOfService": "brukervilkårene", + "and": "og", + "privacyPolicy": "personvernerklæringen" + }, + "siteRequired": "Område er påkrevd.", + "olmTunnel": "Olm-tunnel", + "olmTunnelDescription": "Bruk Olm for klienttilkobling", + "errorCreatingClient": "Feil ved oppretting av klient", + "clientDefaultsNotFound": "Klientstandarder ikke funnet", + "createClient": "Opprett klient", + "createClientDescription": "Opprett en ny klient for å koble til dine områder", + "seeAllClients": "Se alle klienter", + "clientInformation": "Klientinformasjon", + "clientNamePlaceholder": "Klientnavn", + "address": "Adresse", + "subnetPlaceholder": "Subnett", + "addressDescription": "Adressen denne klienten vil bruke for tilkobling", + "selectSites": "Velg områder", + "sitesDescription": "Klienten vil ha tilkobling til de valgte områdene", + "clientInstallOlm": "Installer Olm", + "clientInstallOlmDescription": "Få Olm til å kjøre på systemet ditt", + "clientOlmCredentials": "Olm-legitimasjon", + "clientOlmCredentialsDescription": "Slik vil Olm autentisere med serveren", + "olmEndpoint": "Olm-endepunkt", + "olmId": "Olm-ID", + "olmSecretKey": "Olm hemmelig nøkkel", + "clientCredentialsSave": "Lagre din legitimasjon", + "clientCredentialsSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", + "generalSettingsDescription": "Konfigurer de generelle innstillingene for denne klienten", + "clientUpdated": "Klient oppdatert", + "clientUpdatedDescription": "Klienten er blitt oppdatert.", + "clientUpdateFailed": "Klarte ikke å oppdatere klient", + "clientUpdateError": "En feil oppstod under oppdatering av klienten.", + "sitesFetchFailed": "Klarte ikke å hente områder", + "sitesFetchError": "En feil oppstod under henting av områder.", + "olmErrorFetchReleases": "En feil oppstod under henting av Olm-utgivelser.", + "olmErrorFetchLatest": "En feil oppstod under henting av den nyeste Olm-utgivelsen.", + "remoteSubnets": "Fjern-subnett", + "enterCidrRange": "Skriv inn CIDR-område", + "remoteSubnetsDescription": "Legg til CIDR-områder som kan få fjerntilgang til dette området. Bruk format som 10.0.0.0/24 eller 192.168.1.0/24.", + "resourceEnableProxy": "Aktiver offentlig proxy", + "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", + "externalProxyEnabled": "Ekstern proxy aktivert" +} From d6d2e052dd088b964c7c1a942aacfb5dd4bf8c89 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 10 Aug 2025 10:04:41 -0700 Subject: [PATCH 064/219] Fix missing bracket --- src/components/LocaleSwitcher.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/LocaleSwitcher.tsx b/src/components/LocaleSwitcher.tsx index 82fb5497..ac6a3ced 100644 --- a/src/components/LocaleSwitcher.tsx +++ b/src/components/LocaleSwitcher.tsx @@ -56,6 +56,7 @@ export default function LocaleSwitcher() { { value: "nb-NO", label: "Norsk (Bokmål)" + } ]} /> ); From a829eb949b58de8ab12e26d30d94fc8cea26ba7a Mon Sep 17 00:00:00 2001 From: Marvin <127591405+Lokowitz@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:02:50 +0000 Subject: [PATCH 065/219] modified: package-lock.json modified: package.json modified: server/nextServer.ts --- package-lock.json | 1129 +++++++++--------------------------------- package.json | 4 +- server/nextServer.ts | 2 +- 3 files changed, 229 insertions(+), 906 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ef1b28a..0733801d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "drizzle-orm": "0.44.4", "eslint": "9.32.0", "eslint-config-next": "15.4.6", - "express": "4.21.2", + "express": "5.1.0", "express-rate-limit": "7.5.1", "glob": "11.0.3", "helmet": "8.1.0", @@ -103,7 +103,7 @@ "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", "@types/crypto-js": "^4.2.2", - "@types/express": "5.0.0", + "@types/express": "5.0.3", "@types/express-session": "^1.18.2", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", @@ -760,427 +760,6 @@ "url": "https://github.com/sponsors/nzakas" } }, -<<<<<<< HEAD -======= - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, ->>>>>>> main "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -1292,66 +871,6 @@ "fast-glob": "3.3.1" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", - "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", - "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", - "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", - "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-linux-x64-gnu": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", @@ -1384,36 +903,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", - "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", - "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", @@ -3246,133 +2735,6 @@ "node": ">= 10" } }, -<<<<<<< HEAD -======= - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, ->>>>>>> main "node_modules/@tailwindcss/postcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", @@ -3485,15 +2847,13 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, - "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -4021,6 +3381,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -4175,12 +3536,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -4505,44 +3860,24 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4606,7 +3941,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5005,10 +4339,9 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "dependencies": { "safe-buffer": "5.2.1" }, @@ -5020,7 +4353,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5358,16 +4690,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -5667,8 +4989,7 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -5686,7 +5007,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6059,8 +5379,7 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -6482,7 +5801,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6521,45 +5839,40 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", @@ -6581,6 +5894,18 @@ "express": ">= 4.11" } }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -6590,20 +5915,40 @@ "node": ">= 0.6" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } }, "node_modules/exsolve": { "version": "1.0.7", @@ -6750,38 +6095,21 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6924,12 +6252,11 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs-constants": { @@ -7381,12 +6708,11 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -7783,6 +7109,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8449,19 +7780,20 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -8482,15 +7814,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8516,18 +7839,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8736,6 +8047,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11583,7 +10895,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -11902,7 +11213,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -11948,10 +11258,12 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "engines": { + "node": ">=16" + } }, "node_modules/path-type": { "version": "4.0.0", @@ -12338,12 +11650,11 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -12386,20 +11697,18 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.6.3", "unpipe": "1.0.0" }, "engines": { @@ -12873,6 +12182,21 @@ "node": ">=0.10.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12980,8 +12304,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/scheduler": { "version": "0.26.0", @@ -13014,66 +12337,57 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, "node_modules/set-function-length": { @@ -14154,13 +13468,32 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -14306,7 +13639,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -14426,15 +13758,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/package.json b/package.json index 14013ee8..bf35b6e8 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "drizzle-orm": "0.44.4", "eslint": "9.32.0", "eslint-config-next": "15.4.6", - "express": "4.21.2", + "express": "5.1.0", "express-rate-limit": "7.5.1", "glob": "11.0.3", "helmet": "8.1.0", @@ -121,7 +121,7 @@ "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", "@types/crypto-js": "^4.2.2", - "@types/express": "5.0.0", + "@types/express": "5.0.3", "@types/express-session": "^1.18.2", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", diff --git a/server/nextServer.ts b/server/nextServer.ts index e12c06e6..4c96d04f 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -15,7 +15,7 @@ export async function createNextServer() { const nextServer = express(); - nextServer.all("*", (req, res) => { + nextServer.all("/{*splat}", (req, res) => { const parsedUrl = parse(req.url!, true); return handle(req, res, parsedUrl); }); From 03c8d82471269395d289d3f2f9c6b22d52868bc3 Mon Sep 17 00:00:00 2001 From: jack rosenberg <56937175+jackrosenberg@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:34:40 +0200 Subject: [PATCH 066/219] fix: fixed api error message in createSite.ts --- server/routers/site/createSite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index fb1170cd..fc441b5a 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -144,7 +144,7 @@ export async function createSite( return next( createHttpError( HttpCode.BAD_REQUEST, - "Invalid subnet format. Please provide a valid CIDR notation." + "Invalid address format. Please provide a valid IP notation." ) ); } From 22545cac8b6cf5b539fd18c57b5019b7bf153e10 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 13:40:59 -0700 Subject: [PATCH 067/219] Basic verify session breakout --- server/db/queries/verifySessionQueries.ts | 211 ++++++++++++++++++++++ server/routers/badger/verifySession.ts | 101 +++-------- 2 files changed, 240 insertions(+), 72 deletions(-) create mode 100644 server/db/queries/verifySessionQueries.ts diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts new file mode 100644 index 00000000..1e159304 --- /dev/null +++ b/server/db/queries/verifySessionQueries.ts @@ -0,0 +1,211 @@ +import { db } from "@server/db"; +import { + Resource, + ResourcePassword, + ResourcePincode, + ResourceRule, + resourcePassword, + resourcePincode, + resourceRules, + resources, + roleResources, + sessions, + userOrgs, + userResources, + users +} from "@server/db"; +import { and, eq } from "drizzle-orm"; +import axios from "axios"; + +export type ResourceWithAuth = { + resource: Resource | null; + pincode: ResourcePincode | null; + password: ResourcePassword | null; +}; + +export type UserSessionWithUser = { + session: any; + user: any; +}; + +const MODE = "remote"; +const remoteEndpoint = "https://api.example.com"; + +/** + * Get resource by domain with pincode and password information + */ +export async function getResourceByDomain( + domain: string +): Promise { + if (MODE === "remote") { + try { + const response = await axios.get(`${remoteEndpoint}/resource/domain/${domain}`); + return response.data; + } catch (error) { + console.error("Error fetching resource by domain:", error); + return null; + } + } + + const [result] = await db + .select() + .from(resources) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) + .where(eq(resources.fullDomain, domain)) + .limit(1); + + if (!result) { + return null; + } + + return { + resource: result.resources, + pincode: result.resourcePincode, + password: result.resourcePassword + }; +} + +/** + * Get user session with user information + */ +export async function getUserSessionWithUser( + userSessionId: string +): Promise { + if (MODE === "remote") { + try { + const response = await axios.get(`${remoteEndpoint}/session/${userSessionId}`); + return response.data; + } catch (error) { + console.error("Error fetching user session:", error); + return null; + } + } + + const [res] = await db + .select() + .from(sessions) + .leftJoin(users, eq(users.userId, sessions.userId)) + .where(eq(sessions.sessionId, userSessionId)); + + if (!res) { + return null; + } + + return { + session: res.session, + user: res.user + }; +} + +/** + * Get user organization role + */ +export async function getUserOrgRole(userId: string, orgId: string) { + if (MODE === "remote") { + try { + const response = await axios.get(`${remoteEndpoint}/user/${userId}/org/${orgId}/role`); + return response.data; + } catch (error) { + console.error("Error fetching user org role:", error); + return null; + } + } + + const userOrgRole = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userId), + eq(userOrgs.orgId, orgId) + ) + ) + .limit(1); + + return userOrgRole.length > 0 ? userOrgRole[0] : null; +} + +/** + * Check if role has access to resource + */ +export async function getRoleResourceAccess(resourceId: number, roleId: number) { + if (MODE === "remote") { + try { + const response = await axios.get(`${remoteEndpoint}/role/${roleId}/resource/${resourceId}/access`); + return response.data; + } catch (error) { + console.error("Error fetching role resource access:", error); + return null; + } + } + + const roleResourceAccess = await db + .select() + .from(roleResources) + .where( + and( + eq(roleResources.resourceId, resourceId), + eq(roleResources.roleId, roleId) + ) + ) + .limit(1); + + return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null; +} + +/** + * Check if user has direct access to resource + */ +export async function getUserResourceAccess(userId: string, resourceId: number) { + if (MODE === "remote") { + try { + const response = await axios.get(`${remoteEndpoint}/user/${userId}/resource/${resourceId}/access`); + return response.data; + } catch (error) { + console.error("Error fetching user resource access:", error); + return null; + } + } + + const userResourceAccess = await db + .select() + .from(userResources) + .where( + and( + eq(userResources.userId, userId), + eq(userResources.resourceId, resourceId) + ) + ) + .limit(1); + + return userResourceAccess.length > 0 ? userResourceAccess[0] : null; +} + +/** + * Get resource rules for a given resource + */ +export async function getResourceRules(resourceId: number): Promise { + if (MODE === "remote") { + try { + const response = await axios.get(`${remoteEndpoint}/resource/${resourceId}/rules`); + return response.data; + } catch (error) { + console.error("Error fetching resource rules:", error); + return []; + } + } + + const rules = await db + .select() + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)); + + return rules; +} diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 48d7c064..54a2e0c9 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -6,20 +6,21 @@ import { } from "@server/auth/sessions/resource"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { db } from "@server/db"; +import { + getResourceByDomain, + getUserSessionWithUser, + getUserOrgRole, + getRoleResourceAccess, + getUserResourceAccess, + getResourceRules +} from "@server/db/queries/verifySessionQueries"; import { Resource, ResourceAccessToken, ResourcePassword, - resourcePassword, ResourcePincode, - resourcePincode, ResourceRule, - resourceRules, - resources, - roleResources, sessions, - userOrgs, - userResources, users } from "@server/db"; import config from "@server/lib/config"; @@ -27,7 +28,6 @@ import { isIpInCidr } from "@server/lib/ip"; import { response } from "@server/lib/response"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; -import { and, eq } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import NodeCache from "node-cache"; @@ -137,31 +137,14 @@ export async function verifyResourceSession( | undefined = cache.get(resourceCacheKey); if (!resourceData) { - const [result] = await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - .where(eq(resources.fullDomain, cleanHost)) - .limit(1); + const result = await getResourceByDomain(cleanHost); if (!result) { logger.debug("Resource not found", cleanHost); return notAllowed(res); } - resourceData = { - resource: result.resources, - pincode: result.resourcePincode, - password: result.resourcePassword - }; - + resourceData = result; cache.set(resourceCacheKey, resourceData); } @@ -529,14 +512,13 @@ async function isUserAllowedToAccessResource( userSessionId: string, resource: Resource ): Promise { - const [res] = await db - .select() - .from(sessions) - .leftJoin(users, eq(users.userId, sessions.userId)) - .where(eq(sessions.sessionId, userSessionId)); + const result = await getUserSessionWithUser(userSessionId); - const user = res.user; - const session = res.session; + if (!result) { + return null; + } + + const { user, session } = result; if (!user || !session) { return null; @@ -549,33 +531,18 @@ async function isUserAllowedToAccessResource( return null; } - const userOrgRole = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.userId, user.userId), - eq(userOrgs.orgId, resource.orgId) - ) - ) - .limit(1); + const userOrgRole = await getUserOrgRole(user.userId, resource.orgId); - if (userOrgRole.length === 0) { + if (!userOrgRole) { return null; } - const roleResourceAccess = await db - .select() - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resource.resourceId), - eq(roleResources.roleId, userOrgRole[0].roleId) - ) - ) - .limit(1); + const roleResourceAccess = await getRoleResourceAccess( + resource.resourceId, + userOrgRole.roleId + ); - if (roleResourceAccess.length > 0) { + if (roleResourceAccess) { return { username: user.username, email: user.email, @@ -583,18 +550,12 @@ async function isUserAllowedToAccessResource( }; } - const userResourceAccess = await db - .select() - .from(userResources) - .where( - and( - eq(userResources.userId, user.userId), - eq(userResources.resourceId, resource.resourceId) - ) - ) - .limit(1); + const userResourceAccess = await getUserResourceAccess( + user.userId, + resource.resourceId + ); - if (userResourceAccess.length > 0) { + if (userResourceAccess) { return { username: user.username, email: user.email, @@ -615,11 +576,7 @@ async function checkRules( let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey); if (!rules) { - rules = await db - .select() - .from(resourceRules) - .where(eq(resourceRules.resourceId, resourceId)); - + rules = await getResourceRules(resourceId); cache.set(ruleCacheKey, rules); } From 15f900317a87978f51dca346c13d57164437b61e Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 13:53:57 -0700 Subject: [PATCH 068/219] Basic client --- server/routers/ws/client.ts | 342 ++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 server/routers/ws/client.ts diff --git a/server/routers/ws/client.ts b/server/routers/ws/client.ts new file mode 100644 index 00000000..2cd5cfd7 --- /dev/null +++ b/server/routers/ws/client.ts @@ -0,0 +1,342 @@ +import WebSocket from 'ws'; +import axios from 'axios'; +import { URL } from 'url'; +import { EventEmitter } from 'events'; + +export interface Config { + id: string; + secret: string; + endpoint: string; +} + +export interface WSMessage { + type: string; + data: any; +} + +export interface TokenResponse { + success: boolean; + message?: string; + data: { + token: string; + }; +} + +export type MessageHandler = (message: WSMessage) => void; + +export interface ClientOptions { + baseURL?: string; + reconnectInterval?: number; + pingInterval?: number; + pingTimeout?: number; +} + +export class WebSocketClient extends EventEmitter { + private conn: WebSocket | null = null; + private config: Config; + private baseURL: string; + private handlers: Map = new Map(); + private reconnectInterval: number; + private isConnected: boolean = false; + private pingInterval: number; + private pingTimeout: number; + private clientType: string; + private shouldReconnect: boolean = true; + private reconnectTimer: NodeJS.Timeout | null = null; + private pingTimer: NodeJS.Timeout | null = null; + private pingTimeoutTimer: NodeJS.Timeout | null = null; + + constructor( + clientType: string, + id: string, + secret: string, + endpoint: string, + options: ClientOptions = {} + ) { + super(); + + this.clientType = clientType; + this.config = { + id, + secret, + endpoint + }; + + this.baseURL = options.baseURL || endpoint; + this.reconnectInterval = options.reconnectInterval || 3000; + this.pingInterval = options.pingInterval || 30000; + this.pingTimeout = options.pingTimeout || 10000; + } + + public getConfig(): Config { + return this.config; + } + + public async connect(): Promise { + this.shouldReconnect = true; + await this.connectWithRetry(); + } + + public async close(): Promise { + this.shouldReconnect = false; + + // Clear timers + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.pingTimer) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + if (this.pingTimeoutTimer) { + clearTimeout(this.pingTimeoutTimer); + this.pingTimeoutTimer = null; + } + + if (this.conn) { + this.conn.close(1000, 'Client closing'); + this.conn = null; + } + + this.setConnected(false); + } + + public sendMessage(messageType: string, data: any): Promise { + return new Promise((resolve, reject) => { + if (!this.conn || this.conn.readyState !== WebSocket.OPEN) { + reject(new Error('Not connected')); + return; + } + + const message: WSMessage = { + type: messageType, + data: data + }; + + console.debug(`Sending message: ${messageType}`, data); + + this.conn.send(JSON.stringify(message), (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + + public sendMessageInterval( + messageType: string, + data: any, + interval: number + ): () => void { + // Send immediately + this.sendMessage(messageType, data).catch(err => { + console.error('Failed to send initial message:', err); + }); + + // Set up interval + const intervalId = setInterval(() => { + this.sendMessage(messageType, data).catch(err => { + console.error('Failed to send message:', err); + }); + }, interval); + + // Return stop function + return () => { + clearInterval(intervalId); + }; + } + + public registerHandler(messageType: string, handler: MessageHandler): void { + this.handlers.set(messageType, handler); + } + + public unregisterHandler(messageType: string): void { + this.handlers.delete(messageType); + } + + public isClientConnected(): boolean { + return this.isConnected; + } + + private async getToken(): Promise { + const baseURL = new URL(this.baseURL); + const tokenEndpoint = `${baseURL.origin}/api/v1/auth/${this.clientType}/get-token`; + + const tokenData = this.clientType === 'newt' + ? { newtId: this.config.id, secret: this.config.secret } + : { olmId: this.config.id, secret: this.config.secret }; + + try { + const response = await axios.post(tokenEndpoint, tokenData, { + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': 'x-csrf-protection' + }, + timeout: 10000 // 10 second timeout + }); + + if (!response.data.success) { + throw new Error(`Failed to get token: ${response.data.message}`); + } + + if (!response.data.data.token) { + throw new Error('Received empty token from server'); + } + + console.debug(`Received token: ${response.data.data.token}`); + return response.data.data.token; + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response) { + throw new Error(`Failed to get token with status code: ${error.response.status}`); + } else if (error.request) { + throw new Error('Failed to request new token: No response received'); + } else { + throw new Error(`Failed to request new token: ${error.message}`); + } + } else { + throw new Error(`Failed to get token: ${error}`); + } + } + } + + private async connectWithRetry(): Promise { + while (this.shouldReconnect) { + try { + await this.establishConnection(); + return; + } catch (error) { + console.error(`Failed to connect: ${error}. Retrying in ${this.reconnectInterval}ms...`); + + if (!this.shouldReconnect) return; + + await new Promise(resolve => { + this.reconnectTimer = setTimeout(resolve, this.reconnectInterval); + }); + } + } + } + + private async establishConnection(): Promise { + // Get token for authentication + const token = await this.getToken(); + this.emit('tokenUpdate', token); + + // Parse the base URL to determine protocol and hostname + const baseURL = new URL(this.baseURL); + const wsProtocol = baseURL.protocol === 'https:' ? 'wss' : 'ws'; + const wsURL = new URL(`${wsProtocol}://${baseURL.host}/api/v1/ws`); + + // Add token and client type to query parameters + wsURL.searchParams.set('token', token); + wsURL.searchParams.set('clientType', this.clientType); + + return new Promise((resolve, reject) => { + const conn = new WebSocket(wsURL.toString()); + + conn.on('open', () => { + console.debug('WebSocket connection established'); + this.conn = conn; + this.setConnected(true); + this.startPingMonitor(); + this.emit('connect'); + resolve(); + }); + + conn.on('message', (data: WebSocket.Data) => { + try { + const message: WSMessage = JSON.parse(data.toString()); + const handler = this.handlers.get(message.type); + if (handler) { + handler(message); + } + this.emit('message', message); + } catch (error) { + console.error('Failed to parse message:', error); + } + }); + + conn.on('close', (code, reason) => { + console.debug(`WebSocket connection closed: ${code} ${reason}`); + this.handleDisconnect(); + }); + + conn.on('error', (error) => { + console.error('WebSocket error:', error); + if (this.conn === null) { + // Connection failed during establishment + reject(error); + } else { + this.handleDisconnect(); + } + }); + + conn.on('pong', () => { + if (this.pingTimeoutTimer) { + clearTimeout(this.pingTimeoutTimer); + this.pingTimeoutTimer = null; + } + }); + }); + } + + private startPingMonitor(): void { + this.pingTimer = setInterval(() => { + if (this.conn && this.conn.readyState === WebSocket.OPEN) { + this.conn.ping(); + + // Set timeout for pong response + this.pingTimeoutTimer = setTimeout(() => { + console.error('Ping timeout - no pong received'); + this.handleDisconnect(); + }, this.pingTimeout); + } + }, this.pingInterval); + } + + private handleDisconnect(): void { + this.setConnected(false); + + // Clear ping timers + if (this.pingTimer) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + if (this.pingTimeoutTimer) { + clearTimeout(this.pingTimeoutTimer); + this.pingTimeoutTimer = null; + } + + if (this.conn) { + this.conn.removeAllListeners(); + this.conn = null; + } + + this.emit('disconnect'); + + // Reconnect if needed + if (this.shouldReconnect) { + this.connectWithRetry(); + } + } + + private setConnected(status: boolean): void { + this.isConnected = status; + } +} + +// Factory function for easier instantiation +export function createWebSocketClient( + clientType: string, + id: string, + secret: string, + endpoint: string, + options?: ClientOptions +): WebSocketClient { + return new WebSocketClient(clientType, id, secret, endpoint, options); +} + +export default WebSocketClient; \ No newline at end of file From b6c2f123e8672cdc3a80ffeabc2d675e86d1ac38 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 14:30:23 -0700 Subject: [PATCH 069/219] Add basic ws client --- server/hybridClientServer.ts | 76 ++++++++++++++++++++++++++++++++++++ server/index.ts | 9 ++++- server/lib/readConfigFile.ts | 8 +++- 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 server/hybridClientServer.ts diff --git a/server/hybridClientServer.ts b/server/hybridClientServer.ts new file mode 100644 index 00000000..939fc5fe --- /dev/null +++ b/server/hybridClientServer.ts @@ -0,0 +1,76 @@ +import next from "next"; +import express from "express"; +import { parse } from "url"; +import logger from "@server/logger"; +import config from "@server/lib/config"; +import { WebSocketClient, createWebSocketClient } from "./routers/ws/client"; +import { addPeer, deletePeer } from "./routers/gerbil/peers"; +import { db, exitNodes } from "./db"; + +export async function createHybridClientServer() { + if ( + !config.getRawConfig().hybrid?.id || + !config.getRawConfig().hybrid?.secret || + !config.getRawConfig().hybrid?.endpoint + ) { + throw new Error("Hybrid configuration is not defined"); + } + + // Create client + const client = createWebSocketClient( + "remoteExitNode", // or 'olm' + config.getRawConfig().hybrid!.id!, + config.getRawConfig().hybrid!.secret!, + config.getRawConfig().hybrid!.endpoint!, + { + reconnectInterval: 5000, + pingInterval: 30000, + pingTimeout: 10000 + } + ); + + // Register message handlers + client.registerHandler("remote/peers/add", async (message) => { + const { pubKey, allowedIps } = message.data; + + // TODO: we are getting the exit node twice here + // NOTE: there should only be one gerbil registered so... + const [exitNode] = await db.select().from(exitNodes).limit(1); + await addPeer(exitNode.exitNodeId, { + publicKey: pubKey, + allowedIps: allowedIps || [] + }); + }); + + client.registerHandler("remote/peers/remove", async (message) => { + const { pubKey } = message.data; + + // TODO: we are getting the exit node twice here + // NOTE: there should only be one gerbil registered so... + const [exitNode] = await db.select().from(exitNodes).limit(1); + await deletePeer(exitNode.exitNodeId, pubKey); + }); + + // Listen to connection events + client.on("connect", () => { + console.log("Connected to WebSocket server"); + }); + + client.on("disconnect", () => { + console.log("Disconnected from WebSocket server"); + }); + + client.on("message", (message) => { + console.log("Received message:", message.type, message.data); + }); + + // Connect to the server + try { + await client.connect(); + console.log("Connection initiated"); + } catch (error) { + console.error("Failed to connect:", error); + } + + client.sendMessageInterval("heartbeat", { timestamp: Date.now() }, 10000); +} diff --git a/server/index.ts b/server/index.ts index d3f90281..b0d6d3d7 100644 --- a/server/index.ts +++ b/server/index.ts @@ -7,6 +7,7 @@ import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db"; import { createIntegrationApiServer } from "./integrationApiServer"; +import { createHybridClientServer } from "./hybridClientServer"; import config from "@server/lib/config"; async function startServers() { @@ -18,6 +19,11 @@ async function startServers() { const internalServer = createInternalServer(); const nextServer = await createNextServer(); + let hybridClientServer; + if (config.getRawConfig().hybrid) { + hybridClientServer = createHybridClientServer(); + } + let integrationServer; if (config.getRawConfig().flags?.enable_integration_api) { integrationServer = createIntegrationApiServer(); @@ -27,7 +33,8 @@ async function startServers() { apiServer, nextServer, internalServer, - integrationServer + integrationServer, + hybridClientServer }; } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 1bc119fa..e6e7c548 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -4,6 +4,7 @@ import { configFilePath1, configFilePath2 } from "./consts"; import { z } from "zod"; import stoi from "./stoi"; import { build } from "@server/build"; +import { setAdminCredentials } from "@cli/commands/setAdminCredentials"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -25,8 +26,13 @@ export const configSchema = z .optional() .default("info"), save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false) + log_failed_attempts: z.boolean().optional().default(false), }), + hybrid: z.object({ + id: z.string().optional(), + secret: z.string().optional(), + endpoint: z.string().optional() + }).optional(), domains: z .record( z.string(), From 30dbabd73d234be7b52f7187bb7adc590d8273e8 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 15:27:03 -0700 Subject: [PATCH 070/219] Add internal proxy for gerbil endpoints --- server/routers/gerbil/index.ts | 3 +- server/routers/gerbil/proxy.ts | 101 +++++++++++++++++++++++++++++++++ server/routers/internal.ts | 16 ++++-- 3 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 server/routers/gerbil/proxy.ts diff --git a/server/routers/gerbil/index.ts b/server/routers/gerbil/index.ts index 4a4f3b60..7cf4dfaa 100644 --- a/server/routers/gerbil/index.ts +++ b/server/routers/gerbil/index.ts @@ -1,4 +1,5 @@ export * from "./getConfig"; export * from "./receiveBandwidth"; export * from "./updateHolePunch"; -export * from "./getAllRelays"; \ No newline at end of file +export * from "./getAllRelays"; +export { default as proxyRouter } from "./proxy"; \ No newline at end of file diff --git a/server/routers/gerbil/proxy.ts b/server/routers/gerbil/proxy.ts new file mode 100644 index 00000000..9a6eb98e --- /dev/null +++ b/server/routers/gerbil/proxy.ts @@ -0,0 +1,101 @@ +import { Request, Response, NextFunction } from "express"; +import { Router } from "express"; +import axios from "axios"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import config from "@server/lib/config"; + +const proxyRouter = Router(); + +/** + * Proxy function that forwards requests to the remote cloud server + */ +async function proxyToRemote( + req: Request, + res: Response, + next: NextFunction, + endpoint: string +): Promise { + try { + const remoteConfig = config.getRawConfig().hybrid; + + if (!remoteConfig?.endpoint) { + logger.error("Remote endpoint not configured in hybrid.endpoint config"); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Remote endpoint not configured" + ) + ); + } + + const remoteUrl = `${remoteConfig.endpoint.replace(/\/$/, '')}/api/v1/gerbil/${endpoint}`; + + logger.debug(`Proxying request to remote server: ${remoteUrl}`); + + // Forward the request to the remote server + const response = await axios({ + method: req.method as any, + url: remoteUrl, + data: req.body, + headers: { + 'Content-Type': 'application/json', + }, + params: req.query, + timeout: 30000, // 30 second timeout + validateStatus: () => true // Don't throw on non-2xx status codes + }); + + // Forward the response status and data + return res.status(response.status).json(response.data); + + } catch (error) { + logger.error("Error proxying request to remote server:", error); + + if (axios.isAxiosError(error)) { + if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { + return next( + createHttpError( + HttpCode.SERVICE_UNAVAILABLE, + "Remote server is unavailable" + ) + ); + } + if (error.code === 'ECONNABORTED') { + return next( + createHttpError( + HttpCode.REQUEST_TIMEOUT, + "Request to remote server timed out" + ) + ); + } + } + + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error communicating with remote server" + ) + ); + } +} + +// Proxy endpoints for each gerbil route +proxyRouter.post("/get-config", (req, res, next) => + proxyToRemote(req, res, next, "get-config") +); + +proxyRouter.post("/receive-bandwidth", (req, res, next) => + proxyToRemote(req, res, next, "receive-bandwidth") +); + +proxyRouter.post("/update-hole-punch", (req, res, next) => + proxyToRemote(req, res, next, "update-hole-punch") +); + +proxyRouter.post("/get-all-relays", (req, res, next) => + proxyToRemote(req, res, next, "get-all-relays") +); + +export default proxyRouter; diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 118c8ae3..3fa32d7c 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -7,6 +7,8 @@ import * as auth from "@server/routers/auth"; import * as supporterKey from "@server/routers/supporterKey"; import * as license from "@server/routers/license"; import * as idp from "@server/routers/idp"; +import proxyRouter from "@server/routers/gerbil/proxy"; +import config from "@server/lib/config"; import HttpCode from "@server/types/HttpCode"; import { verifyResourceAccess, @@ -49,10 +51,16 @@ internalRouter.get("/idp/:idpId", idp.getIdp); const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); -gerbilRouter.post("/get-config", gerbil.getConfig); -gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); -gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); -gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); +if (config.getRawConfig().hybrid) { + // Use proxy router to forward requests to remote cloud server + gerbilRouter.use("/", proxyRouter); +} else { + // Use local gerbil endpoints + gerbilRouter.post("/get-config", gerbil.getConfig); + gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); + gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); + gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); +} // Badger routes const badgerRouter = Router(); From 25ed3d65f891b128817b25f6bc597880a68810de Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 15:58:20 -0700 Subject: [PATCH 071/219] Make the proxy more general --- .../gerbil/proxy.ts => remoteProxy.ts} | 30 ++++--------------- server/routers/gerbil/index.ts | 3 +- server/routers/internal.ts | 28 +++++++++++++++-- 3 files changed, 31 insertions(+), 30 deletions(-) rename server/{routers/gerbil/proxy.ts => remoteProxy.ts} (79%) diff --git a/server/routers/gerbil/proxy.ts b/server/remoteProxy.ts similarity index 79% rename from server/routers/gerbil/proxy.ts rename to server/remoteProxy.ts index 9a6eb98e..4e70dd04 100644 --- a/server/routers/gerbil/proxy.ts +++ b/server/remoteProxy.ts @@ -6,17 +6,16 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import config from "@server/lib/config"; -const proxyRouter = Router(); - /** * Proxy function that forwards requests to the remote cloud server */ -async function proxyToRemote( + +export const proxyToRemote = async ( req: Request, res: Response, next: NextFunction, endpoint: string -): Promise { +): Promise => { try { const remoteConfig = config.getRawConfig().hybrid; @@ -30,7 +29,7 @@ async function proxyToRemote( ); } - const remoteUrl = `${remoteConfig.endpoint.replace(/\/$/, '')}/api/v1/gerbil/${endpoint}`; + const remoteUrl = `${remoteConfig.endpoint.replace(/\/$/, '')}/api/v1/${endpoint}`; logger.debug(`Proxying request to remote server: ${remoteUrl}`); @@ -79,23 +78,4 @@ async function proxyToRemote( ) ); } -} - -// Proxy endpoints for each gerbil route -proxyRouter.post("/get-config", (req, res, next) => - proxyToRemote(req, res, next, "get-config") -); - -proxyRouter.post("/receive-bandwidth", (req, res, next) => - proxyToRemote(req, res, next, "receive-bandwidth") -); - -proxyRouter.post("/update-hole-punch", (req, res, next) => - proxyToRemote(req, res, next, "update-hole-punch") -); - -proxyRouter.post("/get-all-relays", (req, res, next) => - proxyToRemote(req, res, next, "get-all-relays") -); - -export default proxyRouter; +} \ No newline at end of file diff --git a/server/routers/gerbil/index.ts b/server/routers/gerbil/index.ts index 7cf4dfaa..4a4f3b60 100644 --- a/server/routers/gerbil/index.ts +++ b/server/routers/gerbil/index.ts @@ -1,5 +1,4 @@ export * from "./getConfig"; export * from "./receiveBandwidth"; export * from "./updateHolePunch"; -export * from "./getAllRelays"; -export { default as proxyRouter } from "./proxy"; \ No newline at end of file +export * from "./getAllRelays"; \ No newline at end of file diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 3fa32d7c..dc212b8b 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -7,7 +7,7 @@ import * as auth from "@server/routers/auth"; import * as supporterKey from "@server/routers/supporterKey"; import * as license from "@server/routers/license"; import * as idp from "@server/routers/idp"; -import proxyRouter from "@server/routers/gerbil/proxy"; +import { proxyToRemote } from "@server/remoteProxy"; import config from "@server/lib/config"; import HttpCode from "@server/types/HttpCode"; import { @@ -53,7 +53,22 @@ internalRouter.use("/gerbil", gerbilRouter); if (config.getRawConfig().hybrid) { // Use proxy router to forward requests to remote cloud server - gerbilRouter.use("/", proxyRouter); + // Proxy endpoints for each gerbil route + gerbilRouter.post("/get-config", (req, res, next) => + proxyToRemote(req, res, next, "gerbil/get-config") + ); + + gerbilRouter.post("/receive-bandwidth", (req, res, next) => + proxyToRemote(req, res, next, "gerbil/receive-bandwidth") + ); + + gerbilRouter.post("/update-hole-punch", (req, res, next) => + proxyToRemote(req, res, next, "gerbil/update-hole-punch") + ); + + gerbilRouter.post("/get-all-relays", (req, res, next) => + proxyToRemote(req, res, next, "gerbil/get-all-relays") + ); } else { // Use local gerbil endpoints gerbilRouter.post("/get-config", gerbil.getConfig); @@ -67,6 +82,13 @@ const badgerRouter = Router(); internalRouter.use("/badger", badgerRouter); badgerRouter.post("/verify-session", badger.verifyResourceSession); -badgerRouter.post("/exchange-session", badger.exchangeSession); + +if (config.getRawConfig().hybrid) { + badgerRouter.post("/exchange-session", (req, res, next) => + proxyToRemote(req, res, next, "badger/exchange-session") + ); +} else { + badgerRouter.post("/exchange-session", badger.exchangeSession); +} export default internalRouter; From f219f1e36b88f6e01fa9e7f74b5511c470779655 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 16:27:34 -0700 Subject: [PATCH 072/219] Move remote proxy --- server/{ => lib}/remoteProxy.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/{ => lib}/remoteProxy.ts (100%) diff --git a/server/remoteProxy.ts b/server/lib/remoteProxy.ts similarity index 100% rename from server/remoteProxy.ts rename to server/lib/remoteProxy.ts From 39e35bc1d6e5e1f64ce7640f95deaff22a9a72ab Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 16:27:41 -0700 Subject: [PATCH 073/219] Add traefik config management --- server/lib/readConfigFile.ts | 34 +- server/lib/remoteTraefikConfig.ts | 582 ++++++++++++++++++++++++++++++ 2 files changed, 607 insertions(+), 9 deletions(-) create mode 100644 server/lib/remoteTraefikConfig.ts diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index e6e7c548..5fb7b955 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -26,13 +26,15 @@ export const configSchema = z .optional() .default("info"), save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false), + log_failed_attempts: z.boolean().optional().default(false) }), - hybrid: z.object({ - id: z.string().optional(), - secret: z.string().optional(), - endpoint: z.string().optional() - }).optional(), + hybrid: z + .object({ + id: z.string().optional(), + secret: z.string().optional(), + endpoint: z.string().optional() + }) + .optional(), domains: z .record( z.string(), @@ -136,7 +138,18 @@ export const configSchema = z https_entrypoint: z.string().optional().default("websecure"), additional_middlewares: z.array(z.string()).optional(), cert_resolver: z.string().optional().default("letsencrypt"), - prefer_wildcard_cert: z.boolean().optional().default(false) + prefer_wildcard_cert: z.boolean().optional().default(false), + certificates_path: z.string().default("./certificates"), + monitor_interval: z.number().default(5000), + dynamic_cert_config_path: z + .string() + .optional() + .default("./dynamic/cert_config.yml"), + dynamic_router_config_path: z + .string() + .optional() + .default("./dynamic/router_config.yml"), + staticDomains: z.array(z.string()).optional().default([]) }) .optional() .default({}), @@ -219,7 +232,10 @@ export const configSchema = z smtp_host: z.string().optional(), smtp_port: portSchema.optional(), smtp_user: z.string().optional(), - smtp_pass: z.string().optional().transform(getEnvOrYaml("EMAIL_SMTP_PASS")), + smtp_pass: z + .string() + .optional() + .transform(getEnvOrYaml("EMAIL_SMTP_PASS")), smtp_secure: z.boolean().optional(), smtp_tls_reject_unauthorized: z.boolean().optional(), no_reply: z.string().email().optional() @@ -235,7 +251,7 @@ export const configSchema = z disable_local_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(), - enable_clients: z.boolean().optional().default(true), + enable_clients: z.boolean().optional().default(true) }) .optional(), dns: z diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts new file mode 100644 index 00000000..755a14ae --- /dev/null +++ b/server/lib/remoteTraefikConfig.ts @@ -0,0 +1,582 @@ +import * as fs from "fs"; +import * as path from "path"; +import config from "@server/lib/config"; +import logger from "@server/logger"; +import * as yaml from "js-yaml"; +import axios from "axios"; +import { db, exitNodes } from "@server/db"; + +export class TraefikConfigManager { + private intervalId: NodeJS.Timeout | null = null; + private isRunning = false; + private activeDomains = new Set(); + private timeoutId: NodeJS.Timeout | null = null; + + constructor() {} + + /** + * Start monitoring certificates + */ + private scheduleNextExecution(): void { + const intervalMs = config.getRawConfig().traefik.monitor_interval; + const now = Date.now(); + const nextExecution = Math.ceil(now / intervalMs) * intervalMs; + const delay = nextExecution - now; + + this.timeoutId = setTimeout(async () => { + try { + await this.HandleTraefikConfig(); + } catch (error) { + logger.error("Error during certificate monitoring:", error); + } + + if (this.isRunning) { + this.scheduleNextExecution(); // Schedule the next execution + } + }, delay); + } + + async start(): Promise { + if (this.isRunning) { + logger.info("Certificate monitor is already running"); + return; + } + this.isRunning = true; + logger.info(`Starting certificate monitor for exit node`); + + // Ensure certificates directory exists + await this.ensureDirectoryExists( + config.getRawConfig().traefik.certificates_path + ); + + // Run initial check + await this.HandleTraefikConfig(); + + // Start synchronized scheduling + this.scheduleNextExecution(); + + logger.info( + `Certificate monitor started with synchronized ${ + config.getRawConfig().traefik.monitor_interval + }ms interval` + ); + } + /** + * Stop monitoring certificates + */ + stop(): void { + if (!this.isRunning) { + logger.info("Certificate monitor is not running"); + return; + } + + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + this.isRunning = false; + logger.info("Certificate monitor stopped"); + } + + /** + * Main monitoring logic + */ + lastActiveDomains: Set = new Set(); + public async HandleTraefikConfig(): Promise { + try { + // Get all active domains for this exit node via HTTP call + const getActiveDomainsFromTraefik = + await this.getActiveDomainsFromTraefik(); + + if (!getActiveDomainsFromTraefik) { + logger.error( + "Failed to fetch active domains from traefik config" + ); + return; + } + + const { domains, traefikConfig } = getActiveDomainsFromTraefik; + + // Add static domains from config + // const staticDomains = [config.getRawConfig().app.dashboard_url]; + // staticDomains.forEach((domain) => domains.add(domain)); + + // Log if domains changed + if ( + this.lastActiveDomains.size !== domains.size || + !Array.from(this.lastActiveDomains).every((domain) => + domains.has(domain) + ) + ) { + logger.info( + `Active domains changed for exit node: ${Array.from(domains).join(", ")}` + ); + this.lastActiveDomains = new Set(domains); + } + + // Get valid certificates for active domains + const validCertificates = + await this.getValidCertificatesForDomains(domains); + + // Download and decrypt new certificates + await this.processValidCertificates(validCertificates); + + // Clean up certificates for domains no longer in use + await this.cleanupUnusedCertificates(domains); + + // wait 1 second for traefik to pick up the new certificates + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Write traefik config as YAML to a second dynamic config file if changed + await this.writeTraefikDynamicConfig(traefikConfig); + + // Send domains to SNI proxy + try { + const [exitNode] = await db.select().from(exitNodes).limit(1); + if (exitNode) { + logger.error("No exit node found"); + await axios.post( + `${exitNode.reachableAt}/full-domains`, + { fullDomains: Array.from(domains) }, + { headers: { "Content-Type": "application/json" } } + ); + } + } catch (err) { + logger.error("Failed to post domains to SNI proxy:", err); + } + + // Update active domains tracking + this.activeDomains = domains; + } catch (error) { + logger.error("Error in certificate monitoring cycle:", error); + } + } + + /** + * Get all domains currently in use from traefik config API + */ + private async getActiveDomainsFromTraefik(): Promise<{ + domains: Set; + traefikConfig: any; + } | null> { + try { + const resp = await axios.get( + `${config.getRawConfig().hybrid?.endpoint}/get-traefik-config` + ); + + if (resp.status !== 200) { + logger.error( + `Failed to fetch traefik config: ${resp.status} ${resp.statusText}` + ); + return null; + } + + const traefikConfig = resp.data; + const domains = new Set(); + + if (traefikConfig?.http?.routers) { + for (const router of Object.values( + traefikConfig.http.routers + )) { + if (router.rule && typeof router.rule === "string") { + // Match Host(`domain`) + const match = router.rule.match(/Host\(`([^`]+)`\)/); + if (match && match[1]) { + domains.add(match[1]); + } + } + } + } + return { domains, traefikConfig }; + } catch (err) { + logger.error("Failed to fetch traefik config:", err); + return null; + } + } + + /** + * Write traefik config as YAML to a second dynamic config file if changed + */ + private async writeTraefikDynamicConfig(traefikConfig: any): Promise { + const traefikDynamicConfigPath = + config.getRawConfig().traefik.dynamic_router_config_path; + let shouldWrite = false; + let oldJson = ""; + if (fs.existsSync(traefikDynamicConfigPath)) { + try { + const oldContent = fs.readFileSync( + traefikDynamicConfigPath, + "utf8" + ); + // Try to parse as YAML then JSON.stringify for comparison + const oldObj = yaml.load(oldContent); + oldJson = JSON.stringify(oldObj); + } catch { + oldJson = ""; + } + } + const newJson = JSON.stringify(traefikConfig); + if (oldJson !== newJson) { + shouldWrite = true; + } + if (shouldWrite) { + try { + fs.writeFileSync( + traefikDynamicConfigPath, + yaml.dump(traefikConfig, { noRefs: true }), + "utf8" + ); + logger.info("Traefik dynamic config updated"); + } catch (err) { + logger.error("Failed to write traefik dynamic config:", err); + } + } + } + + /** + * Get valid certificates for the specified domains + */ + private async getValidCertificatesForDomains(domains: Set): Promise< + Array<{ + id: number; + domain: string; + certFile: string | null; + keyFile: string | null; + expiresAt: Date | null; + updatedAt?: Date | null; + }> + > { + if (domains.size === 0) { + return []; + } + + const domainArray = Array.from(domains); + + try { + const response = await axios.get( + `${config.getRawConfig().hybrid?.endpoint}/certificates/domains`, + { + params: { + domains: domainArray + } + } + ); + return response.data; + } catch (error) { + console.error("Error fetching resource by domain:", error); + return []; + } + } + + /** + * Process valid certificates - download and decrypt them + */ + private async processValidCertificates( + validCertificates: Array<{ + id: number; + domain: string; + certFile: string | null; + keyFile: string | null; + expiresAt: Date | null; + updatedAt?: Date | null; + }> + ): Promise { + const dynamicConfigPath = + config.getRawConfig().traefik.dynamic_cert_config_path; + + // Load existing dynamic config if it exists, otherwise initialize + let dynamicConfig: any = { tls: { certificates: [] } }; + if (fs.existsSync(dynamicConfigPath)) { + try { + const fileContent = fs.readFileSync(dynamicConfigPath, "utf8"); + dynamicConfig = yaml.load(fileContent) || dynamicConfig; + if (!dynamicConfig.tls) + dynamicConfig.tls = { certificates: [] }; + if (!Array.isArray(dynamicConfig.tls.certificates)) { + dynamicConfig.tls.certificates = []; + } + } catch (err) { + logger.error("Failed to load existing dynamic config:", err); + } + } + + // Keep a copy of the original config for comparison + const originalConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); + + for (const cert of validCertificates) { + try { + if (!cert.certFile || !cert.keyFile) { + logger.warn( + `Certificate for domain ${cert.domain} is missing cert or key file` + ); + continue; + } + + const domainDir = path.join( + config.getRawConfig().traefik.certificates_path, + cert.domain + ); + await this.ensureDirectoryExists(domainDir); + + const certPath = path.join(domainDir, "cert.pem"); + const keyPath = path.join(domainDir, "key.pem"); + const lastUpdatePath = path.join(domainDir, ".last_update"); + + // Check if we need to update the certificate + const shouldUpdate = await this.shouldUpdateCertificate( + cert, + certPath, + keyPath, + lastUpdatePath + ); + + if (shouldUpdate) { + logger.info( + `Processing certificate for domain: ${cert.domain}` + ); + + fs.writeFileSync(certPath, cert.certFile, "utf8"); + fs.writeFileSync(keyPath, cert.keyFile, "utf8"); + + // Set appropriate permissions (readable by owner only for key file) + fs.chmodSync(certPath, 0o644); + fs.chmodSync(keyPath, 0o600); + + // Write/update .last_update file with current timestamp + fs.writeFileSync( + lastUpdatePath, + new Date().toISOString(), + "utf8" + ); + + logger.info( + `Certificate updated for domain: ${cert.domain}` + ); + } + + // Always ensure the config entry exists and is up to date + const certEntry = { + certFile: `/var/${certPath}`, + keyFile: `/var/${keyPath}` + }; + // Remove any existing entry for this cert/key path + dynamicConfig.tls.certificates = + dynamicConfig.tls.certificates.filter( + (entry: any) => + entry.certFile !== certEntry.certFile || + entry.keyFile !== certEntry.keyFile + ); + dynamicConfig.tls.certificates.push(certEntry); + } catch (error) { + logger.error( + `Error processing certificate for domain ${cert.domain}:`, + error + ); + } + } + + // Only write the config if it has changed + const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); + if (newConfigYaml !== originalConfigYaml) { + fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); + logger.info("Dynamic cert config updated"); + } + } + + /** + * Check if certificate should be updated + */ + private async shouldUpdateCertificate( + cert: { + id: number; + domain: string; + expiresAt: Date | null; + updatedAt?: Date | null; + }, + certPath: string, + keyPath: string, + lastUpdatePath: string + ): Promise { + try { + // If files don't exist, we need to create them + const certExists = await this.fileExists(certPath); + const keyExists = await this.fileExists(keyPath); + const lastUpdateExists = await this.fileExists(lastUpdatePath); + + if (!certExists || !keyExists || !lastUpdateExists) { + return true; + } + + // Read last update time from .last_update file + let lastUpdateTime: Date | null = null; + try { + const lastUpdateStr = fs + .readFileSync(lastUpdatePath, "utf8") + .trim(); + lastUpdateTime = new Date(lastUpdateStr); + } catch { + lastUpdateTime = null; + } + + // Use updatedAt from cert, fallback to expiresAt if not present + const dbUpdateTime = cert.updatedAt ?? cert.expiresAt; + + if (!dbUpdateTime) { + // If no update time in DB, always update + return true; + } + + // If DB updatedAt is newer than last update file, update + if (!lastUpdateTime || dbUpdateTime > lastUpdateTime) { + return true; + } + + return false; + } catch (error) { + logger.error( + `Error checking certificate update status for ${cert.domain}:`, + error + ); + return true; // When in doubt, update + } + } + + /** + * Clean up certificates for domains no longer in use + */ + private async cleanupUnusedCertificates( + currentActiveDomains: Set + ): Promise { + try { + const certsPath = config.getRawConfig().traefik.certificates_path; + const dynamicConfigPath = + config.getRawConfig().traefik.dynamic_cert_config_path; + + // Load existing dynamic config if it exists + let dynamicConfig: any = { tls: { certificates: [] } }; + if (fs.existsSync(dynamicConfigPath)) { + try { + const fileContent = fs.readFileSync( + dynamicConfigPath, + "utf8" + ); + dynamicConfig = yaml.load(fileContent) || dynamicConfig; + if (!dynamicConfig.tls) + dynamicConfig.tls = { certificates: [] }; + if (!Array.isArray(dynamicConfig.tls.certificates)) { + dynamicConfig.tls.certificates = []; + } + } catch (err) { + logger.error( + "Failed to load existing dynamic config:", + err + ); + } + } + + const certDirs = fs.readdirSync(certsPath, { + withFileTypes: true + }); + + let configChanged = false; + + for (const dirent of certDirs) { + if (!dirent.isDirectory()) continue; + + const dirName = dirent.name; + // Only delete if NO current domain is exactly the same or ends with `.${dirName}` + const shouldDelete = !Array.from(currentActiveDomains).some( + (domain) => + domain === dirName || domain.endsWith(`.${dirName}`) + ); + + if (shouldDelete) { + const domainDir = path.join(certsPath, dirName); + logger.info( + `Cleaning up unused certificate directory: ${dirName}` + ); + fs.rmSync(domainDir, { recursive: true, force: true }); + + // Remove from dynamic config + const certFilePath = `/var/${path.join( + domainDir, + "cert.pem" + )}`; + const keyFilePath = `/var/${path.join( + domainDir, + "key.pem" + )}`; + const before = dynamicConfig.tls.certificates.length; + dynamicConfig.tls.certificates = + dynamicConfig.tls.certificates.filter( + (entry: any) => + entry.certFile !== certFilePath && + entry.keyFile !== keyFilePath + ); + if (dynamicConfig.tls.certificates.length !== before) { + configChanged = true; + } + } + } + + if (configChanged) { + try { + fs.writeFileSync( + dynamicConfigPath, + yaml.dump(dynamicConfig, { noRefs: true }), + "utf8" + ); + logger.info("Dynamic config updated after cleanup"); + } catch (err) { + logger.error( + "Failed to update dynamic config after cleanup:", + err + ); + } + } + } catch (error) { + logger.error("Error during certificate cleanup:", error); + } + } + + /** + * Ensure directory exists + */ + private async ensureDirectoryExists(dirPath: string): Promise { + try { + fs.mkdirSync(dirPath, { recursive: true }); + } catch (error) { + logger.error(`Error creating directory ${dirPath}:`, error); + throw error; + } + } + + /** + * Check if file exists + */ + private async fileExists(filePath: string): Promise { + try { + fs.accessSync(filePath); + return true; + } catch { + return false; + } + } + + /** + * Get current status + */ + getStatus(): { + isRunning: boolean; + activeDomains: string[]; + monitorInterval: number; + } { + return { + isRunning: this.isRunning, + activeDomains: Array.from(this.activeDomains), + monitorInterval: + config.getRawConfig().traefik.monitor_interval || 5000 + }; + } +} From 880a123149937ea41760cb05e870070ef4a62de0 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 16:31:53 -0700 Subject: [PATCH 074/219] Import tcm --- server/hybridClientServer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/hybridClientServer.ts b/server/hybridClientServer.ts index 939fc5fe..8a16f985 100644 --- a/server/hybridClientServer.ts +++ b/server/hybridClientServer.ts @@ -6,8 +6,13 @@ import config from "@server/lib/config"; import { WebSocketClient, createWebSocketClient } from "./routers/ws/client"; import { addPeer, deletePeer } from "./routers/gerbil/peers"; import { db, exitNodes } from "./db"; +import { TraefikConfigManager } from "./lib/remoteTraefikConfig"; export async function createHybridClientServer() { + const monitor = new TraefikConfigManager(); + + await monitor.start(); + if ( !config.getRawConfig().hybrid?.id || !config.getRawConfig().hybrid?.secret || @@ -33,7 +38,7 @@ export async function createHybridClientServer() { client.registerHandler("remote/peers/add", async (message) => { const { pubKey, allowedIps } = message.data; - // TODO: we are getting the exit node twice here + // TODO: we are getting the exit node twice here // NOTE: there should only be one gerbil registered so... const [exitNode] = await db.select().from(exitNodes).limit(1); await addPeer(exitNode.exitNodeId, { @@ -45,7 +50,7 @@ export async function createHybridClientServer() { client.registerHandler("remote/peers/remove", async (message) => { const { pubKey } = message.data; - // TODO: we are getting the exit node twice here + // TODO: we are getting the exit node twice here // NOTE: there should only be one gerbil registered so... const [exitNode] = await db.select().from(exitNodes).limit(1); await deletePeer(exitNode.exitNodeId, pubKey); From 3d8869066ad8226815b14dd444b997f300f6000f Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 16:47:59 -0700 Subject: [PATCH 075/219] Adjust pulling in config --- server/db/queries/verifySessionQueries.ts | 28 +++++++++++------------ server/hybridClientServer.ts | 4 ++++ server/index.ts | 2 +- server/lib/config.ts | 4 ++++ server/lib/remoteProxy.ts | 14 +----------- server/routers/internal.ts | 6 ++--- 6 files changed, 26 insertions(+), 32 deletions(-) diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 1e159304..44982f64 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -16,6 +16,7 @@ import { } from "@server/db"; import { and, eq } from "drizzle-orm"; import axios from "axios"; +import config from "@server/lib/config"; export type ResourceWithAuth = { resource: Resource | null; @@ -28,18 +29,15 @@ export type UserSessionWithUser = { user: any; }; -const MODE = "remote"; -const remoteEndpoint = "https://api.example.com"; - /** * Get resource by domain with pincode and password information */ export async function getResourceByDomain( domain: string ): Promise { - if (MODE === "remote") { + if (config.isHybridMode()) { try { - const response = await axios.get(`${remoteEndpoint}/resource/domain/${domain}`); + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/resource/domain/${domain}`); return response.data; } catch (error) { console.error("Error fetching resource by domain:", error); @@ -78,9 +76,9 @@ export async function getResourceByDomain( export async function getUserSessionWithUser( userSessionId: string ): Promise { - if (MODE === "remote") { + if (config.isHybridMode()) { try { - const response = await axios.get(`${remoteEndpoint}/session/${userSessionId}`); + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/session/${userSessionId}`); return response.data; } catch (error) { console.error("Error fetching user session:", error); @@ -108,9 +106,9 @@ export async function getUserSessionWithUser( * Get user organization role */ export async function getUserOrgRole(userId: string, orgId: string) { - if (MODE === "remote") { + if (config.isHybridMode()) { try { - const response = await axios.get(`${remoteEndpoint}/user/${userId}/org/${orgId}/role`); + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/user/${userId}/org/${orgId}/role`); return response.data; } catch (error) { console.error("Error fetching user org role:", error); @@ -136,9 +134,9 @@ export async function getUserOrgRole(userId: string, orgId: string) { * Check if role has access to resource */ export async function getRoleResourceAccess(resourceId: number, roleId: number) { - if (MODE === "remote") { + if (config.isHybridMode()) { try { - const response = await axios.get(`${remoteEndpoint}/role/${roleId}/resource/${resourceId}/access`); + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/role/${roleId}/resource/${resourceId}/access`); return response.data; } catch (error) { console.error("Error fetching role resource access:", error); @@ -164,9 +162,9 @@ export async function getRoleResourceAccess(resourceId: number, roleId: number) * Check if user has direct access to resource */ export async function getUserResourceAccess(userId: string, resourceId: number) { - if (MODE === "remote") { + if (config.isHybridMode()) { try { - const response = await axios.get(`${remoteEndpoint}/user/${userId}/resource/${resourceId}/access`); + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/user/${userId}/resource/${resourceId}/access`); return response.data; } catch (error) { console.error("Error fetching user resource access:", error); @@ -192,9 +190,9 @@ export async function getUserResourceAccess(userId: string, resourceId: number) * Get resource rules for a given resource */ export async function getResourceRules(resourceId: number): Promise { - if (MODE === "remote") { + if (config.isHybridMode()) { try { - const response = await axios.get(`${remoteEndpoint}/resource/${resourceId}/rules`); + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/resource/${resourceId}/rules`); return response.data; } catch (error) { console.error("Error fetching resource rules:", error); diff --git a/server/hybridClientServer.ts b/server/hybridClientServer.ts index 8a16f985..074fcd2e 100644 --- a/server/hybridClientServer.ts +++ b/server/hybridClientServer.ts @@ -56,6 +56,10 @@ export async function createHybridClientServer() { await deletePeer(exitNode.exitNodeId, pubKey); }); + client.registerHandler("remote/traefik/reload", async (message) => { + await monitor.HandleTraefikConfig(); + }); + // Listen to connection events client.on("connect", () => { console.log("Connected to WebSocket server"); diff --git a/server/index.ts b/server/index.ts index b0d6d3d7..42f85da6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,7 +20,7 @@ async function startServers() { const nextServer = await createNextServer(); let hybridClientServer; - if (config.getRawConfig().hybrid) { + if (config.isHybridMode()) { hybridClientServer = createHybridClientServer(); } diff --git a/server/lib/config.ts b/server/lib/config.ts index 023ae054..c8c7b7c4 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -147,6 +147,10 @@ export class Config { return false; } + public isHybridMode() { + return this.rawConfig?.hybrid; + } + public async checkSupporterKey() { const [key] = await db.select().from(supporterKey).limit(1); diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts index 4e70dd04..080c3bd3 100644 --- a/server/lib/remoteProxy.ts +++ b/server/lib/remoteProxy.ts @@ -17,20 +17,8 @@ export const proxyToRemote = async ( endpoint: string ): Promise => { try { - const remoteConfig = config.getRawConfig().hybrid; - - if (!remoteConfig?.endpoint) { - logger.error("Remote endpoint not configured in hybrid.endpoint config"); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Remote endpoint not configured" - ) - ); - } + const remoteUrl = `${config.getRawConfig().hybrid?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`; - const remoteUrl = `${remoteConfig.endpoint.replace(/\/$/, '')}/api/v1/${endpoint}`; - logger.debug(`Proxying request to remote server: ${remoteUrl}`); // Forward the request to the remote server diff --git a/server/routers/internal.ts b/server/routers/internal.ts index dc212b8b..a84f6976 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -7,7 +7,7 @@ import * as auth from "@server/routers/auth"; import * as supporterKey from "@server/routers/supporterKey"; import * as license from "@server/routers/license"; import * as idp from "@server/routers/idp"; -import { proxyToRemote } from "@server/remoteProxy"; +import { proxyToRemote } from "@server/lib/remoteProxy"; import config from "@server/lib/config"; import HttpCode from "@server/types/HttpCode"; import { @@ -51,7 +51,7 @@ internalRouter.get("/idp/:idpId", idp.getIdp); const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); -if (config.getRawConfig().hybrid) { +if (config.isHybridMode()) { // Use proxy router to forward requests to remote cloud server // Proxy endpoints for each gerbil route gerbilRouter.post("/get-config", (req, res, next) => @@ -83,7 +83,7 @@ internalRouter.use("/badger", badgerRouter); badgerRouter.post("/verify-session", badger.verifyResourceSession); -if (config.getRawConfig().hybrid) { +if (config.isHybridMode()) { badgerRouter.post("/exchange-session", (req, res, next) => proxyToRemote(req, res, next, "badger/exchange-session") ); From af638d666cf490277493e000a224510b8758f944 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 21:34:07 -0700 Subject: [PATCH 076/219] Dont look for port if not root; causes permission --- install/main.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/install/main.go b/install/main.go index d380591b..b08f0073 100644 --- a/install/main.go +++ b/install/main.go @@ -77,16 +77,16 @@ func main() { fmt.Println("Lets get started!") fmt.Println("") + if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS + for _, p := range []int{80, 443} { + if err := checkPortsAvailable(p); err != nil { + fmt.Fprintln(os.Stderr, err) - for _, p := range []int{80, 443} { - if err := checkPortsAvailable(p); err != nil { - fmt.Fprintln(os.Stderr, err) - - fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly") - os.Exit(1) - } - } - + fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly") + os.Exit(1) + } + } + } reader := bufio.NewReader(os.Stdin) inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") From c6d78680fb8b5bc144461b26994107cfbef3d393 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:03 -0700 Subject: [PATCH 077/219] New translations en-us.json (Norwegian Bokmal) --- messages/nb-NO.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 92b52d01..f2b0924b 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer", "pincodeRequirementsChars": "PIN må kun inneholde tall", "passwordRequirementsLength": "Passord må være minst 1 tegn langt", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.", "otpEmailSent": "OTP sendt", "otpEmailSentDescription": "En OTP er sendt til din e-post", @@ -967,6 +985,9 @@ "actionDeleteSite": "Slett område", "actionGetSite": "Hent område", "actionListSites": "List opp områder", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Oppdater område", "actionListSiteRoles": "List opp tillatte områderoller", "actionCreateResource": "Opprett ressurs", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Aktiver offentlig proxy", "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", "externalProxyEnabled": "Ekstern proxy aktivert" -} +} \ No newline at end of file From cea7190453b2de11a5fa126ae2b03775f3df7833 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:04 -0700 Subject: [PATCH 078/219] New translations en-us.json (French) --- messages/fr-FR.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index bb1a4ac3..16e286d9 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres", "pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres", "passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère", "otpEmailSent": "OTP envoyé", "otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail", @@ -967,6 +985,9 @@ "actionDeleteSite": "Supprimer un site", "actionGetSite": "Obtenir un site", "actionListSites": "Lister les sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Mettre à jour un site", "actionListSiteRoles": "Lister les rôles autorisés du site", "actionCreateResource": "Créer une ressource", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Activer le proxy public", "resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.", "externalProxyEnabled": "Proxy externe activé" -} +} \ No newline at end of file From cf12d3ee56b1931c722323dde1cea1c7f4967929 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:06 -0700 Subject: [PATCH 079/219] New translations en-us.json (Spanish) --- messages/es-ES.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index 7fabb18c..3f862cea 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos", "pincodeRequirementsChars": "El PIN sólo debe contener números", "passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter", "otpEmailSent": "OTP enviado", "otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico", @@ -967,6 +985,9 @@ "actionDeleteSite": "Eliminar sitio", "actionGetSite": "Obtener sitio", "actionListSites": "Listar sitios", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Actualizar sitio", "actionListSiteRoles": "Lista de roles permitidos del sitio", "actionCreateResource": "Crear Recurso", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Habilitar proxy público", "resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.", "externalProxyEnabled": "Proxy externo habilitado" -} +} \ No newline at end of file From 0a13b04c55ed69774fd6065e20ee99b78cc0d4de Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:07 -0700 Subject: [PATCH 080/219] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 738fe3ed..1d982bc6 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN must be exactly 6 digits", "pincodeRequirementsChars": "PIN must only contain numbers", "passwordRequirementsLength": "Password must be at least 1 character long", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP must be at least 1 character long", "otpEmailSent": "OTP Sent", "otpEmailSentDescription": "An OTP has been sent to your email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From e9c2868998b6de1833dbac83dd310949b93f09cb Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:09 -0700 Subject: [PATCH 081/219] New translations en-us.json (Czech) --- messages/cs-CZ.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 6fe79036..d21f37c2 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN must be exactly 6 digits", "pincodeRequirementsChars": "PIN must only contain numbers", "passwordRequirementsLength": "Password must be at least 1 character long", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP must be at least 1 character long", "otpEmailSent": "OTP Sent", "otpEmailSentDescription": "An OTP has been sent to your email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From 9dc73efa3a96f1ac77acf3539e110c5d59c847c8 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:10 -0700 Subject: [PATCH 082/219] New translations en-us.json (German) --- messages/de-DE.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index e82fb44a..fab7e28a 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein", "pincodeRequirementsChars": "PIN darf nur Zahlen enthalten", "passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein", "otpEmailSent": "OTP gesendet", "otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet", @@ -967,6 +985,9 @@ "actionDeleteSite": "Standort löschen", "actionGetSite": "Standort abrufen", "actionListSites": "Standorte auflisten", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Standorte aktualisieren", "actionListSiteRoles": "Erlaubte Standort-Rollen auflisten", "actionCreateResource": "Ressource erstellen", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Öffentlichen Proxy aktivieren", "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", "externalProxyEnabled": "Externer Proxy aktiviert" -} +} \ No newline at end of file From 5f36b13408eb8a21d4468b5b0c20f08be08a3c9b Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:11 -0700 Subject: [PATCH 083/219] New translations en-us.json (Italian) --- messages/it-IT.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index 651259eb..82753fc7 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre", "pincodeRequirementsChars": "Il PIN deve contenere solo numeri", "passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere", "otpEmailSent": "OTP Inviato", "otpEmailSentDescription": "Un OTP è stato inviato alla tua email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Elimina Sito", "actionGetSite": "Ottieni Sito", "actionListSites": "Elenca Siti", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Aggiorna Sito", "actionListSiteRoles": "Elenca Ruoli Sito Consentiti", "actionCreateResource": "Crea Risorsa", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Abilita Proxy Pubblico", "resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.", "externalProxyEnabled": "Proxy Esterno Abilitato" -} +} \ No newline at end of file From c70eaa00968f1fa27772d89eada7690b83b0fedd Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:13 -0700 Subject: [PATCH 084/219] New translations en-us.json (Korean) --- messages/ko-KR.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 0c28db0f..4e6fb851 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다", "pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.", "passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다", "otpEmailSent": "OTP 전송됨", "otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.", @@ -967,6 +985,9 @@ "actionDeleteSite": "사이트 삭제", "actionGetSite": "사이트 가져오기", "actionListSites": "사이트 목록", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "사이트 업데이트", "actionListSiteRoles": "허용된 사이트 역할 목록", "actionCreateResource": "리소스 생성", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From 168056d595d0ee41b526a4928f7f72ef07790cc5 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:14 -0700 Subject: [PATCH 085/219] New translations en-us.json (Dutch) --- messages/nl-NL.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 68ccfeae..aa8859cf 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn", "pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten", "passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn", "otpEmailSent": "OTP verzonden", "otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden", @@ -967,6 +985,9 @@ "actionDeleteSite": "Site verwijderen", "actionGetSite": "Site ophalen", "actionListSites": "Sites weergeven", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Site bijwerken", "actionListSiteRoles": "Toon toegestane sitenollen", "actionCreateResource": "Bron maken", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Openbare proxy inschakelen", "resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.", "externalProxyEnabled": "Externe Proxy Ingeschakeld" -} +} \ No newline at end of file From 5f09f970322e392a6d579d6dffc04421341df064 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:15 -0700 Subject: [PATCH 086/219] New translations en-us.json (Polish) --- messages/pl-PL.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 0df783a5..edf39a6a 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr", "pincodeRequirementsChars": "PIN może zawierać tylko cyfry", "passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak", "otpEmailSent": "Kod jednorazowy wysłany", "otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail", @@ -967,6 +985,9 @@ "actionDeleteSite": "Usuń witrynę", "actionGetSite": "Pobierz witrynę", "actionListSites": "Lista witryn", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Aktualizuj witrynę", "actionListSiteRoles": "Lista dozwolonych ról witryny", "actionCreateResource": "Utwórz zasób", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Włącz publiczny proxy", "resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.", "externalProxyEnabled": "Zewnętrzny Proxy Włączony" -} +} \ No newline at end of file From c8dda4f90dc87998561b27c5039cbc19e10fbc46 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:17 -0700 Subject: [PATCH 087/219] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index c126ba1c..ad32ce79 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos", "pincodeRequirementsChars": "O PIN deve conter apenas números", "passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere", "otpEmailSent": "OTP Enviado", "otpEmailSentDescription": "Um OTP foi enviado para o seu email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Eliminar Site", "actionGetSite": "Obter Site", "actionListSites": "Listar Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Atualizar Site", "actionListSiteRoles": "Listar Funções Permitidas do Site", "actionCreateResource": "Criar Recurso", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Ativar Proxy Público", "resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.", "externalProxyEnabled": "Proxy Externo Habilitado" -} +} \ No newline at end of file From 40f520086c15d749326c047a75811088e4d0b8ca Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:18 -0700 Subject: [PATCH 088/219] New translations en-us.json (Russian) --- messages/ru-RU.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 62360ecc..f9a49a3f 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр", "pincodeRequirementsChars": "PIN должен содержать только цифры", "passwordRequirementsLength": "Пароль должен быть не менее 1 символа", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP должен быть не менее 1 символа", "otpEmailSent": "OTP отправлен", "otpEmailSentDescription": "OTP был отправлен на ваш email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Удалить сайт", "actionGetSite": "Получить сайт", "actionListSites": "Список сайтов", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Обновить сайт", "actionListSiteRoles": "Список разрешенных ролей сайта", "actionCreateResource": "Создать ресурс", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From 75f97c4a31af331e65c86a54403c942f56331ee2 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:20 -0700 Subject: [PATCH 089/219] New translations en-us.json (Turkish) --- messages/tr-TR.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 8b9e2450..103a94a5 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır", "pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır", "passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır", "otpEmailSent": "OTP Gönderildi", "otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi", @@ -967,6 +985,9 @@ "actionDeleteSite": "Siteyi Sil", "actionGetSite": "Siteyi Al", "actionListSites": "Siteleri Listele", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Siteyi Güncelle", "actionListSiteRoles": "İzin Verilen Site Rolleri Listele", "actionCreateResource": "Kaynak Oluştur", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Genel Proxy'i Etkinleştir", "resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.", "externalProxyEnabled": "Dış Proxy Etkinleştirildi" -} +} \ No newline at end of file From 297991ef5f1ecc2f6102ece1cb0054632fd814b6 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:21 -0700 Subject: [PATCH 090/219] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 6172738c..b7b29307 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN码必须是6位数字", "pincodeRequirementsChars": "PIN 必须只包含数字", "passwordRequirementsLength": "密码必须至少 1 个字符长", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP 必须至少 1 个字符长", "otpEmailSent": "OTP 已发送", "otpEmailSentDescription": "OTP 已经发送到您的电子邮件", @@ -967,6 +985,9 @@ "actionDeleteSite": "删除站点", "actionGetSite": "获取站点", "actionListSites": "站点列表", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "更新站点", "actionListSiteRoles": "允许站点角色列表", "actionCreateResource": "创建资源", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "启用公共代理", "resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。", "externalProxyEnabled": "外部代理已启用" -} +} \ No newline at end of file From 0f50981573831394c9c3ba431f120e46784ed092 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 11:15:06 -0700 Subject: [PATCH 091/219] Update lock --- package-lock.json | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3c16f842..647db0bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1670,6 +1670,66 @@ "fast-glob": "3.3.1" } }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", + "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", + "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", + "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", + "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-x64-gnu": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", @@ -1702,6 +1762,36 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", + "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", + "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", From c244dc9c0cc689d20929fdd3fdf5c5de71af01c0 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 11:15:14 -0700 Subject: [PATCH 092/219] Add accept clients to install --- messages/en-US.json | 7 +- .../[orgId]/settings/sites/create/page.tsx | 114 ++++++++++-------- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index f8b3f8b9..6ac72d8a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1344,5 +1344,10 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "externalProxyEnabled": "External Proxy Enabled", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept client connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to." } \ No newline at end of file diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index d6ab64d0..4d5171f1 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -206,8 +206,10 @@ PersistentKeepalive = 5`; acceptClients: boolean = false ) => { const acceptClientsFlag = acceptClients ? " --accept-clients" : ""; - const acceptClientsEnv = acceptClients ? "\n - ACCEPT_CLIENTS=true" : ""; - + const acceptClientsEnv = acceptClients + ? "\n - ACCEPT_CLIENTS=true" + : ""; + const commands = { mac: { "Apple Silicon (arm64)": [ @@ -388,7 +390,7 @@ WantedBy=default.target` case "freebsd": return ; case "nixos": - return ; + return ; default: return ; } @@ -566,6 +568,11 @@ WantedBy=default.target` load(); }, []); + // Sync form acceptClients value with local state + useEffect(() => { + form.setValue("acceptClients", acceptClients); + }, [acceptClients, form]); + return ( <>
@@ -626,7 +633,7 @@ WantedBy=default.target` render={({ field }) => ( - Site Address + {t("siteAddress")} - Specify the - IP address - of the host - for clients - to connect - to. + {t("siteAddressDescription")} )} /> )} - {form.watch("method") === - "newt" && ( - ( - -
- { - const value = checked as boolean; - field.onChange(value); - setAcceptClients(value); - // Re-hydrate commands with new acceptClients value - if (newtId && newtSecret) { - hydrateCommands( - newtId, - newtSecret, - env.app.dashboardUrl, - newtVersion, - value - ); - } - }} - /> - -
- - Allow other devices to connect through this newt instance as a gateway. - - -
- )} - /> - )} @@ -903,6 +863,56 @@ WantedBy=default.target` ) )}
+ +
+

+ {t("siteConfiguration")} +

+
+ { + const value = + checked as boolean; + setAcceptClients( + value + ); + form.setValue( + "acceptClients", + value + ); + // Re-hydrate commands with new acceptClients value + if ( + newtId && + newtSecret && + newtVersion + ) { + hydrateCommands( + newtId, + newtSecret, + env.app + .dashboardUrl, + newtVersion, + value + ); + } + }} + /> + +
+

+ {t("siteAcceptClientConnectionsDescription")} +

+
+

{t("commands")} From 50fc2fc74e80fed95fdb01cb33ee0a09e7a0516a Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 11:30:21 -0700 Subject: [PATCH 093/219] Add newt install command --- messages/en-US.json | 2 +- .../[orgId]/settings/sites/create/page.tsx | 108 ++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 6ac72d8a..1a3fdfa8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1346,7 +1346,7 @@ "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled", "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept client connections", + "siteAcceptClientConnections": "Accept Client Connections", "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", "siteAddress": "Site Address", "siteAddressDescription": "Specify the IP address of the host for clients to connect to." diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 4d5171f1..c1c97c44 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -43,7 +43,7 @@ import { FaWindows } from "react-icons/fa"; import { SiNixos } from "react-icons/si"; -import { Checkbox } from "@app/components/ui/checkbox"; +import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -72,6 +72,7 @@ interface TunnelTypeOption { type Commands = { mac: Record; linux: Record; + freebsd: Record; windows: Record; docker: Record; podman: Record; @@ -212,46 +213,46 @@ PersistentKeepalive = 5`; const commands = { mac: { - "Apple Silicon (arm64)": [ - `curl -L -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_darwin_arm64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - "Intel x64 (amd64)": [ - `curl -L -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_darwin_amd64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + All: [ + `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] + // "Intel x64 (amd64)": [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] }, linux: { - amd64: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_amd64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm64: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm32: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm32" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm32v6: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm32v6" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - riscv64: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_riscv64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + All: [ + `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] + // arm64: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ], + // arm32: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ], + // arm32v6: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ], + // riscv64: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] }, freebsd: { - amd64: [ - `fetch -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_freebsd_amd64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm64: [ - `fetch -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_freebsd_arm64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + All: [ + `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] + // arm64: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] }, windows: { x64: [ @@ -299,12 +300,12 @@ WantedBy=default.target` ] }, nixos: { - x86_64: [ + All: [ `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ], - aarch64: [ - `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ] + // aarch64: [ + // `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] } }; setCommands(commands); @@ -313,9 +314,11 @@ WantedBy=default.target` const getArchitectures = () => { switch (platform) { case "linux": - return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; + // return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; + return ["All"]; case "mac": - return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; + // return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; + return ["All"]; case "windows": return ["x64"]; case "docker": @@ -323,9 +326,11 @@ WantedBy=default.target` case "podman": return ["Podman Quadlet", "Podman Run"]; case "freebsd": - return ["amd64", "arm64"]; + // return ["amd64", "arm64"]; + return ["All"]; case "nixos": - return ["x86_64", "aarch64"]; + // return ["x86_64", "aarch64"]; + return ["All"]; default: return ["x64"]; } @@ -633,7 +638,9 @@ WantedBy=default.target` render={({ field }) => ( - {t("siteAddress")} + {t( + "siteAddress" + )} - {t("siteAddressDescription")} + {t( + "siteAddressDescription" + )} )} @@ -869,7 +878,7 @@ WantedBy=default.target` {t("siteConfiguration")}

-

- {t("siteAcceptClientConnectionsDescription")} + {t( + "siteAcceptClientConnectionsDescription" + )}

From 16e876ab68946390a0437012524d3ebad3a24bf7 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:13:47 -0700 Subject: [PATCH 094/219] Clean up checkbox --- src/app/[orgId]/settings/sites/create/page.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index c1c97c44..cdf596cf 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -880,6 +880,7 @@ WantedBy=default.target`
-
-

+

{t( "siteAcceptClientConnectionsDescription" )} From 9987b35b60d613df089cabbf5801be4e833352e9 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:26:38 -0700 Subject: [PATCH 095/219] Update package lock again --- package-lock.json | 2287 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 2063 insertions(+), 224 deletions(-) diff --git a/package-lock.json b/package-lock.json index 647db0bc..e0d69052 100644 --- a/package-lock.json +++ b/package-lock.json @@ -281,9 +281,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -360,6 +360,37 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -1386,31 +1417,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.2", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", - "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.2" + "@floating-ui/dom": "^1.7.3" }, "peerDependencies": { "react": ">=16.8.0", @@ -1559,6 +1590,424 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -1601,7 +2050,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -1611,9 +2059,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -1632,16 +2080,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1655,6 +2103,18 @@ "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@next/env": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz", @@ -1677,6 +2137,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1692,6 +2153,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1707,6 +2169,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1722,6 +2185,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1769,6 +2233,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1784,6 +2249,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1806,9 +2272,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.6.tgz", + "integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1859,6 +2325,134 @@ "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, + "node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-android-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-freebsd-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/argon2-linux-x64-gnu": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", @@ -1891,6 +2485,70 @@ "node": ">= 10" } }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", + "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", + "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", + "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", + "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/bcrypt": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", @@ -1920,6 +2578,134 @@ "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" } }, + "node_modules/@node-rs/bcrypt-android-arm-eabi": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.9.0.tgz", + "integrity": "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-android-arm64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.9.0.tgz", + "integrity": "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-arm64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.9.0.tgz", + "integrity": "sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-x64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.9.0.tgz", + "integrity": "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-freebsd-x64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.9.0.tgz", + "integrity": "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.9.0.tgz", + "integrity": "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.9.0.tgz", + "integrity": "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-musl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.9.0.tgz", + "integrity": "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/bcrypt-linux-x64-gnu": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.9.0.tgz", @@ -1952,6 +2738,103 @@ "node": ">= 10" } }, + "node_modules/@node-rs/bcrypt-wasm32-wasi": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.9.0.tgz", + "integrity": "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/core": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", + "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", + "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz", + "integrity": "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.9.0.tgz", + "integrity": "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-x64-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.9.0.tgz", + "integrity": "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2043,59 +2926,59 @@ "license": "MIT" }, "node_modules/@peculiar/asn1-android": { - "version": "2.3.16", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.16.tgz", - "integrity": "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.4.0.tgz", + "integrity": "sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz", - "integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.4.0.tgz", + "integrity": "sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "@peculiar/asn1-x509": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-x509": "^2.4.0", + "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-rsa": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz", - "integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.4.0.tgz", + "integrity": "sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "@peculiar/asn1-x509": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-x509": "^2.4.0", + "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-schema": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz", - "integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.4.0.tgz", + "integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==", "license": "MIT", "dependencies": { - "asn1js": "^3.0.5", + "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-x509": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz", - "integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.4.0.tgz", + "integrity": "sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } @@ -3590,6 +4473,125 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", @@ -3624,6 +4626,70 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/postcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", @@ -3671,6 +4737,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.12", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", @@ -3740,6 +4816,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4240,6 +5317,175 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", @@ -4266,15 +5512,69 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" @@ -4752,6 +6052,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -4830,6 +6131,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4891,9 +6193,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", "funding": [ { "type": "opencollective", @@ -4956,7 +6258,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -5119,6 +6420,20 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5231,6 +6546,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -5242,6 +6558,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5878,7 +7195,8 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -5896,6 +7214,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5940,6 +7259,20 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/engine.io/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -5968,6 +7301,39 @@ } } }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/engine.io/node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -5991,9 +7357,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -6268,7 +7634,8 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -6690,6 +8057,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6731,6 +8099,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6783,22 +8152,10 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6808,37 +8165,11 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", "engines": { "node": ">=6.6.0" } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -6987,6 +8318,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -7041,9 +8373,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -7119,6 +8451,27 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -7144,6 +8497,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -7154,6 +8508,28 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "license": "Unlicense", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7422,7 +8798,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -7578,6 +8953,15 @@ "node": ">= 0.8" } }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7600,6 +8984,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8001,7 +9386,8 @@ "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", @@ -8174,7 +9560,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -8213,9 +9598,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "devOptional": true, "license": "MIT", "bin": { @@ -8456,6 +9841,132 @@ "lightningcss-win32-x64-msvc": "1.30.1" } }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", @@ -8498,6 +10009,48 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8672,14 +10225,39 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "license": "Unlicense", + "optional": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memfs-browser": { + "version": "3.5.10302", + "resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz", + "integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==", + "license": "Unlicense", + "optional": true, + "dependencies": { + "memfs": "3.5.3" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -8729,21 +10307,21 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -8827,7 +10405,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -8840,7 +10417,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -8912,9 +10488,9 @@ "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.0.tgz", - "integrity": "sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" @@ -8933,10 +10509,9 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -9021,15 +10596,6 @@ } } }, - "node_modules/next-intl/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -11784,6 +13350,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -11885,9 +13452,9 @@ } }, "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", + "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", "dev": true, "license": "MIT", "engines": { @@ -11963,6 +13530,26 @@ "@node-rs/bcrypt": "1.9.0" } }, + "node_modules/oslo/node_modules/@emnapi/core": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", + "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/oslo/node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/oslo/node_modules/@node-rs/argon2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz", @@ -11988,6 +13575,134 @@ "@node-rs/argon2-win32-x64-msvc": "1.7.0" } }, + "node_modules/oslo/node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", + "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-android-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", + "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-darwin-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz", + "integrity": "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-darwin-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", + "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-freebsd-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", + "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", + "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", + "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", + "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz", @@ -12020,6 +13735,83 @@ "node": ">= 10" } }, + "node_modules/oslo/node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", + "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", + "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", + "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", + "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@tybys/wasm-util": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", + "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12102,6 +13894,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -12150,6 +13943,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", "engines": { "node": ">=16" } @@ -12542,6 +14336,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -12586,6 +14381,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -12594,6 +14390,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -12705,9 +14502,9 @@ } }, "node_modules/react-email/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", + "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", "dev": true, "license": "MIT", "engines": { @@ -12727,6 +14524,16 @@ "node": ">=18" } }, + "node_modules/react-email/node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/react-email/node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -12740,29 +14547,6 @@ "node": ">=6" } }, - "node_modules/react-email/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/react-email/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/react-email/node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -13075,6 +14859,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -13193,7 +14978,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/scheduler": { "version": "0.26.0", @@ -13229,6 +15015,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -13246,29 +15033,11 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -13331,6 +15100,49 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13604,6 +15416,20 @@ } } }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/socket.io/node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -13622,6 +15448,39 @@ } } }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13677,9 +15536,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -14002,9 +15861,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.26.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.26.2.tgz", - "integrity": "sha512-WmMS9iMlHQejNm/Uw5ZTo4e3M2QMmEavRz7WLWVsq7Mlx4PSHJbY+VCrLsAz9wLxyHVgrJdt7N8+SdQLa52Ykg==", + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz", + "integrity": "sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -14055,7 +15914,6 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -14360,6 +16218,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -14369,25 +16228,6 @@ "node": ">= 0.6" } }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -14528,6 +16368,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -14711,7 +16552,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -15017,16 +16857,15 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", "bin": { "yaml": "bin.mjs" From e7df29104ea6d9378c1f782a35b2af6ba171a14e Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:28:45 -0700 Subject: [PATCH 096/219] Fix backwards compat path --- server/routers/olm/handleOlmRegisterMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 64443e07..5128e9e1 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -67,7 +67,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // THIS IS FOR BACKWARDS COMPATIBILITY await sendToClient(olm.olmId, { - type: "olm/wg/holepunch/all", + type: "olm/wg/holepunch", data: { serverPubKey: allExitNodes[0].publicKey, endpoint: allExitNodes[0].endpoint From 1f4a7a7f6f82f0a6edb4c4656550c796951ae6ab Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:35:09 -0700 Subject: [PATCH 097/219] Add olm version --- server/db/pg/schema.ts | 1 + server/db/sqlite/schema.ts | 1 + .../routers/olm/handleOlmRegisterMessage.ts | 30 +++++++++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index c7a1eebf..657e22eb 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -536,6 +536,7 @@ export const olms = pgTable("olms", { olmId: varchar("id").primaryKey(), secretHash: varchar("secretHash").notNull(), dateCreated: varchar("dateCreated").notNull(), + version: text("version").notNull(), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 4268cd9f..730ae4ea 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -243,6 +243,7 @@ export const olms = sqliteTable("olms", { olmId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), dateCreated: text("dateCreated").notNull(), + version: text("version").notNull(), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 5128e9e1..7eb3d978 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -21,7 +21,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } const clientId = olm.clientId; - const { publicKey, relay } = message.data; + const { publicKey, relay, olmVersion } = message.data; logger.debug( `Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}` @@ -65,14 +65,26 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } }); - // THIS IS FOR BACKWARDS COMPATIBILITY - await sendToClient(olm.olmId, { - type: "olm/wg/holepunch", - data: { - serverPubKey: allExitNodes[0].publicKey, - endpoint: allExitNodes[0].endpoint - } - }); + if (!olmVersion) { + // THIS IS FOR BACKWARDS COMPATIBILITY + // THE OLDER CLIENTS DID NOT SEND THE VERSION + await sendToClient(olm.olmId, { + type: "olm/wg/holepunch", + data: { + serverPubKey: allExitNodes[0].publicKey, + endpoint: allExitNodes[0].endpoint + } + }); + } + } + + if (olmVersion) { + await db + .update(olms) + .set({ + version: olmVersion + }) + .where(eq(olms.olmId, olm.olmId)); } if (now - (client.lastHolePunch || 0) > 6) { From 4c463de45f31fdd0f4c54e3cf5a72d9f67c7152f Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 14:47:03 -0700 Subject: [PATCH 098/219] Version does not need to be notNull --- server/db/pg/schema.ts | 2 +- server/db/sqlite/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 657e22eb..477636f7 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -536,7 +536,7 @@ export const olms = pgTable("olms", { olmId: varchar("id").primaryKey(), secretHash: varchar("secretHash").notNull(), dateCreated: varchar("dateCreated").notNull(), - version: text("version").notNull(), + version: text("version"), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 730ae4ea..460081a9 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -243,7 +243,7 @@ export const olms = sqliteTable("olms", { olmId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), dateCreated: text("dateCreated").notNull(), - version: text("version").notNull(), + version: text("version"), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) From ddd8eb1da05a3ff6e7c1bd5911a406dd4f0ec5d0 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 16:02:03 -0700 Subject: [PATCH 099/219] Change sni proxy url --- server/lib/remoteTraefikConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index 755a14ae..08b2ab98 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -137,7 +137,7 @@ export class TraefikConfigManager { if (exitNode) { logger.error("No exit node found"); await axios.post( - `${exitNode.reachableAt}/full-domains`, + `${exitNode.reachableAt}/update-local-snis`, { fullDomains: Array.from(domains) }, { headers: { "Content-Type": "application/json" } } ); From 1f6379a7e6d7307cf54f3e64e95b0216db5d994e Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 16:15:23 -0700 Subject: [PATCH 100/219] Break out traefik config --- server/routers/traefik/getTraefikConfig.ts | 847 +++++++++++---------- 1 file changed, 424 insertions(+), 423 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 882a296a..da013878 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -13,434 +13,37 @@ export async function traefikConfigProvider( res: Response ): Promise { try { - // Get all resources with related data - const allResources = await db.transaction(async (tx) => { - // First query to get resources with site and org info - // Get the current exit node name from config - if (!currentExitNodeId) { - if (config.getRawConfig().gerbil.exit_node_name) { - const exitNodeName = - config.getRawConfig().gerbil.exit_node_name!; - const [exitNode] = await tx - .select({ - exitNodeId: exitNodes.exitNodeId - }) - .from(exitNodes) - .where(eq(exitNodes.name, exitNodeName)); - if (exitNode) { - currentExitNodeId = exitNode.exitNodeId; - } - } else { - const [exitNode] = await tx - .select({ - exitNodeId: exitNodes.exitNodeId - }) - .from(exitNodes) - .limit(1); - - if (exitNode) { - currentExitNodeId = exitNode.exitNodeId; - } - } - } - - // Get the site(s) on this exit node - const resourcesWithRelations = await tx - .select({ - // Resource fields - resourceId: resources.resourceId, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol, - subdomain: resources.subdomain, - domainId: resources.domainId, - // Site fields - site: { - siteId: sites.siteId, - type: sites.type, - subnet: sites.subnet, - exitNodeId: sites.exitNodeId - }, - enabled: resources.enabled, - stickySession: resources.stickySession, - tlsServerName: resources.tlsServerName, - setHostHeader: resources.setHostHeader, - enableProxy: resources.enableProxy - }) - .from(resources) - .innerJoin(sites, eq(sites.siteId, resources.siteId)) - .where( - or( - eq(sites.exitNodeId, currentExitNodeId), - isNull(sites.exitNodeId) - ) - ); - - // Get all resource IDs from the first query - const resourceIds = resourcesWithRelations.map((r) => r.resourceId); - - // Second query to get all enabled targets for these resources - const allTargets = - resourceIds.length > 0 - ? await tx - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled - }) - .from(targets) - .where( - and( - inArray(targets.resourceId, resourceIds), - eq(targets.enabled, true) - ) - ) - : []; - - // Create a map for fast target lookup by resourceId - const targetsMap = allTargets.reduce((map, target) => { - if (!map.has(target.resourceId)) { - map.set(target.resourceId, []); - } - map.get(target.resourceId).push(target); - return map; - }, new Map()); - - // Combine the data - return resourcesWithRelations.map((resource) => ({ - ...resource, - targets: targetsMap.get(resource.resourceId) || [] - })); - }); - - if (!allResources.length) { - return res.status(HttpCode.OK).json({}); - } - - const badgerMiddlewareName = "badger"; - const redirectHttpsMiddlewareName = "redirect-to-https"; - - const config_output: any = { - http: { - middlewares: { - [badgerMiddlewareName]: { - plugin: { - [badgerMiddlewareName]: { - apiBaseUrl: new URL( - "/api/v1", - `http://${ - config.getRawConfig().server - .internal_hostname - }:${ - config.getRawConfig().server - .internal_port - }` - ).href, - userSessionCookieName: - config.getRawConfig().server - .session_cookie_name, - - // deprecated - accessTokenQueryParam: - config.getRawConfig().server - .resource_access_token_param, - - resourceSessionRequestParam: - config.getRawConfig().server - .resource_session_request_param - } - } - }, - [redirectHttpsMiddlewareName]: { - redirectScheme: { - scheme: "https" - } - } - } - } - }; - - for (const resource of allResources) { - const targets = resource.targets as Target[]; - const site = resource.site; - - const routerName = `${resource.resourceId}-router`; - const serviceName = `${resource.resourceId}-service`; - const fullDomain = `${resource.fullDomain}`; - const transportName = `${resource.resourceId}-transport`; - const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`; - - if (!resource.enabled) { - continue; - } - - if (resource.http) { - if (!resource.domainId) { - continue; - } - - if (!resource.fullDomain) { - logger.error( - `Resource ${resource.resourceId} has no fullDomain` - ); - continue; - } - - // add routers and services empty objects if they don't exist - if (!config_output.http.routers) { - config_output.http.routers = {}; - } - - if (!config_output.http.services) { - config_output.http.services = {}; - } - - const domainParts = fullDomain.split("."); - let wildCard; - if (domainParts.length <= 2) { - wildCard = `*.${domainParts.join(".")}`; - } else { - wildCard = `*.${domainParts.slice(1).join(".")}`; - } - - if (!resource.subdomain) { - wildCard = resource.fullDomain; - } - - const configDomain = config.getDomain(resource.domainId); - - let certResolver: string, preferWildcardCert: boolean; - if (!configDomain) { - certResolver = config.getRawConfig().traefik.cert_resolver; - preferWildcardCert = - config.getRawConfig().traefik.prefer_wildcard_cert; - } else { - certResolver = configDomain.cert_resolver; - preferWildcardCert = configDomain.prefer_wildcard_cert; - } - - const tls = { - certResolver: certResolver, - ...(preferWildcardCert - ? { - domains: [ - { - main: wildCard - } - ] - } - : {}) - }; - - const additionalMiddlewares = - config.getRawConfig().traefik.additional_middlewares || []; - - config_output.http.routers![routerName] = { - entryPoints: [ - resource.ssl - ? config.getRawConfig().traefik.https_entrypoint - : config.getRawConfig().traefik.http_entrypoint - ], - middlewares: [ - badgerMiddlewareName, - ...additionalMiddlewares - ], - service: serviceName, - rule: `Host(\`${fullDomain}\`)`, - priority: 100, - ...(resource.ssl ? { tls } : {}) - }; - - if (resource.ssl) { - config_output.http.routers![routerName + "-redirect"] = { - entryPoints: [ - config.getRawConfig().traefik.http_entrypoint - ], - middlewares: [redirectHttpsMiddlewareName], - service: serviceName, - rule: `Host(\`${fullDomain}\`)`, - priority: 100 - }; - } - - config_output.http.services![serviceName] = { - loadBalancer: { - servers: targets - .filter((target: Target) => { - if (!target.enabled) { - return false; - } - if ( - site.type === "local" || - site.type === "wireguard" - ) { - if ( - !target.ip || - !target.port || - !target.method - ) { - return false; - } - } else if (site.type === "newt") { - if ( - !target.internalPort || - !target.method || - !site.subnet - ) { - return false; - } - } - return true; - }) - .map((target: Target) => { - if ( - site.type === "local" || - site.type === "wireguard" - ) { - return { - url: `${target.method}://${target.ip}:${target.port}` - }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; - return { - url: `${target.method}://${ip}:${target.internalPort}` - }; - } - }), - ...(resource.stickySession - ? { - sticky: { - cookie: { - name: "p_sticky", // TODO: make this configurable via config.yml like other cookies - secure: resource.ssl, - httpOnly: true - } - } - } - : {}) - } - }; - - // Add the serversTransport if TLS server name is provided - if (resource.tlsServerName) { - if (!config_output.http.serversTransports) { - config_output.http.serversTransports = {}; - } - config_output.http.serversTransports![transportName] = { - serverName: resource.tlsServerName, - //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings - // if defined in the static config and here. if not set, self-signed certs won't work - insecureSkipVerify: true - }; - config_output.http.services![ - serviceName - ].loadBalancer.serversTransport = transportName; - } - - // Add the host header middleware - if (resource.setHostHeader) { - if (!config_output.http.middlewares) { - config_output.http.middlewares = {}; - } - config_output.http.middlewares[hostHeaderMiddlewareName] = { - headers: { - customRequestHeaders: { - Host: resource.setHostHeader - } - } - }; - if (!config_output.http.routers![routerName].middlewares) { - config_output.http.routers![routerName].middlewares = - []; - } - config_output.http.routers![routerName].middlewares = [ - ...config_output.http.routers![routerName].middlewares, - hostHeaderMiddlewareName - ]; + // First query to get resources with site and org info + // Get the current exit node name from config + if (!currentExitNodeId) { + if (config.getRawConfig().gerbil.exit_node_name) { + const exitNodeName = + config.getRawConfig().gerbil.exit_node_name!; + const [exitNode] = await db + .select({ + exitNodeId: exitNodes.exitNodeId + }) + .from(exitNodes) + .where(eq(exitNodes.name, exitNodeName)); + if (exitNode) { + currentExitNodeId = exitNode.exitNodeId; } } else { - // Non-HTTP (TCP/UDP) configuration - if (!resource.enableProxy) { - continue; + const [exitNode] = await db + .select({ + exitNodeId: exitNodes.exitNodeId + }) + .from(exitNodes) + .limit(1); + + if (exitNode) { + currentExitNodeId = exitNode.exitNodeId; } - - const protocol = resource.protocol.toLowerCase(); - const port = resource.proxyPort; - - if (!port) { - continue; - } - - if (!config_output[protocol]) { - config_output[protocol] = { - routers: {}, - services: {} - }; - } - - config_output[protocol].routers[routerName] = { - entryPoints: [`${protocol}-${port}`], - service: serviceName, - ...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {}) - }; - - config_output[protocol].services[serviceName] = { - loadBalancer: { - servers: targets - .filter((target: Target) => { - if (!target.enabled) { - return false; - } - if ( - site.type === "local" || - site.type === "wireguard" - ) { - if (!target.ip || !target.port) { - return false; - } - } else if (site.type === "newt") { - if (!target.internalPort || !site.subnet) { - return false; - } - } - return true; - }) - .map((target: Target) => { - if ( - site.type === "local" || - site.type === "wireguard" - ) { - return { - address: `${target.ip}:${target.port}` - }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; - return { - address: `${ip}:${target.internalPort}` - }; - } - }), - ...(resource.stickySession - ? { - sticky: { - ipStrategy: { - depth: 0, - sourcePort: true - } - } - } - : {}) - } - }; } } - return res.status(HttpCode.OK).json(config_output); + + const traefikConfig = await getTraefikConfig(currentExitNodeId); + return res.status(HttpCode.OK).json(traefikConfig); } catch (e) { logger.error(`Failed to build Traefik config: ${e}`); return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({ @@ -448,3 +51,401 @@ export async function traefikConfigProvider( }); } } + +export async function getTraefikConfig( + exitNodeId: number +): Promise { + // Get all resources with related data + const allResources = await db.transaction(async (tx) => { + + // Get the site(s) on this exit node + const resourcesWithRelations = await tx + .select({ + // Resource fields + resourceId: resources.resourceId, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + http: resources.http, + proxyPort: resources.proxyPort, + protocol: resources.protocol, + subdomain: resources.subdomain, + domainId: resources.domainId, + // Site fields + site: { + siteId: sites.siteId, + type: sites.type, + subnet: sites.subnet, + exitNodeId: sites.exitNodeId + }, + enabled: resources.enabled, + stickySession: resources.stickySession, + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader, + enableProxy: resources.enableProxy + }) + .from(resources) + .innerJoin(sites, eq(sites.siteId, resources.siteId)) + .where( + or( + eq(sites.exitNodeId, exitNodeId), + isNull(sites.exitNodeId) + ) + ); + + // Get all resource IDs from the first query + const resourceIds = resourcesWithRelations.map((r) => r.resourceId); + + // Second query to get all enabled targets for these resources + const allTargets = + resourceIds.length > 0 + ? await tx + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled + }) + .from(targets) + .where( + and( + inArray(targets.resourceId, resourceIds), + eq(targets.enabled, true) + ) + ) + : []; + + // Create a map for fast target lookup by resourceId + const targetsMap = allTargets.reduce((map, target) => { + if (!map.has(target.resourceId)) { + map.set(target.resourceId, []); + } + map.get(target.resourceId).push(target); + return map; + }, new Map()); + + // Combine the data + return resourcesWithRelations.map((resource) => ({ + ...resource, + targets: targetsMap.get(resource.resourceId) || [] + })); + }); + + if (!allResources.length) { + return {} + } + + const badgerMiddlewareName = "badger"; + const redirectHttpsMiddlewareName = "redirect-to-https"; + + const config_output: any = { + http: { + middlewares: { + [badgerMiddlewareName]: { + plugin: { + [badgerMiddlewareName]: { + apiBaseUrl: new URL( + "/api/v1", + `http://${ + config.getRawConfig().server + .internal_hostname + }:${config.getRawConfig().server.internal_port}` + ).href, + userSessionCookieName: + config.getRawConfig().server + .session_cookie_name, + + // deprecated + accessTokenQueryParam: + config.getRawConfig().server + .resource_access_token_param, + + resourceSessionRequestParam: + config.getRawConfig().server + .resource_session_request_param + } + } + }, + [redirectHttpsMiddlewareName]: { + redirectScheme: { + scheme: "https" + } + } + } + } + }; + + for (const resource of allResources) { + const targets = resource.targets as Target[]; + const site = resource.site; + + const routerName = `${resource.resourceId}-router`; + const serviceName = `${resource.resourceId}-service`; + const fullDomain = `${resource.fullDomain}`; + const transportName = `${resource.resourceId}-transport`; + const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`; + + if (!resource.enabled) { + continue; + } + + if (resource.http) { + if (!resource.domainId) { + continue; + } + + if (!resource.fullDomain) { + logger.error( + `Resource ${resource.resourceId} has no fullDomain` + ); + continue; + } + + // add routers and services empty objects if they don't exist + if (!config_output.http.routers) { + config_output.http.routers = {}; + } + + if (!config_output.http.services) { + config_output.http.services = {}; + } + + const domainParts = fullDomain.split("."); + let wildCard; + if (domainParts.length <= 2) { + wildCard = `*.${domainParts.join(".")}`; + } else { + wildCard = `*.${domainParts.slice(1).join(".")}`; + } + + if (!resource.subdomain) { + wildCard = resource.fullDomain; + } + + const configDomain = config.getDomain(resource.domainId); + + let certResolver: string, preferWildcardCert: boolean; + if (!configDomain) { + certResolver = config.getRawConfig().traefik.cert_resolver; + preferWildcardCert = + config.getRawConfig().traefik.prefer_wildcard_cert; + } else { + certResolver = configDomain.cert_resolver; + preferWildcardCert = configDomain.prefer_wildcard_cert; + } + + const tls = { + certResolver: certResolver, + ...(preferWildcardCert + ? { + domains: [ + { + main: wildCard + } + ] + } + : {}) + }; + + const additionalMiddlewares = + config.getRawConfig().traefik.additional_middlewares || []; + + config_output.http.routers![routerName] = { + entryPoints: [ + resource.ssl + ? config.getRawConfig().traefik.https_entrypoint + : config.getRawConfig().traefik.http_entrypoint + ], + middlewares: [badgerMiddlewareName, ...additionalMiddlewares], + service: serviceName, + rule: `Host(\`${fullDomain}\`)`, + priority: 100, + ...(resource.ssl ? { tls } : {}) + }; + + if (resource.ssl) { + config_output.http.routers![routerName + "-redirect"] = { + entryPoints: [ + config.getRawConfig().traefik.http_entrypoint + ], + middlewares: [redirectHttpsMiddlewareName], + service: serviceName, + rule: `Host(\`${fullDomain}\`)`, + priority: 100 + }; + } + + config_output.http.services![serviceName] = { + loadBalancer: { + servers: targets + .filter((target: Target) => { + if (!target.enabled) { + return false; + } + if ( + site.type === "local" || + site.type === "wireguard" + ) { + if ( + !target.ip || + !target.port || + !target.method + ) { + return false; + } + } else if (site.type === "newt") { + if ( + !target.internalPort || + !target.method || + !site.subnet + ) { + return false; + } + } + return true; + }) + .map((target: Target) => { + if ( + site.type === "local" || + site.type === "wireguard" + ) { + return { + url: `${target.method}://${target.ip}:${target.port}` + }; + } else if (site.type === "newt") { + const ip = site.subnet!.split("/")[0]; + return { + url: `${target.method}://${ip}:${target.internalPort}` + }; + } + }), + ...(resource.stickySession + ? { + sticky: { + cookie: { + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies + secure: resource.ssl, + httpOnly: true + } + } + } + : {}) + } + }; + + // Add the serversTransport if TLS server name is provided + if (resource.tlsServerName) { + if (!config_output.http.serversTransports) { + config_output.http.serversTransports = {}; + } + config_output.http.serversTransports![transportName] = { + serverName: resource.tlsServerName, + //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings + // if defined in the static config and here. if not set, self-signed certs won't work + insecureSkipVerify: true + }; + config_output.http.services![ + serviceName + ].loadBalancer.serversTransport = transportName; + } + + // Add the host header middleware + if (resource.setHostHeader) { + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + config_output.http.middlewares[hostHeaderMiddlewareName] = { + headers: { + customRequestHeaders: { + Host: resource.setHostHeader + } + } + }; + if (!config_output.http.routers![routerName].middlewares) { + config_output.http.routers![routerName].middlewares = []; + } + config_output.http.routers![routerName].middlewares = [ + ...config_output.http.routers![routerName].middlewares, + hostHeaderMiddlewareName + ]; + } + } else { + // Non-HTTP (TCP/UDP) configuration + if (!resource.enableProxy) { + continue; + } + + const protocol = resource.protocol.toLowerCase(); + const port = resource.proxyPort; + + if (!port) { + continue; + } + + if (!config_output[protocol]) { + config_output[protocol] = { + routers: {}, + services: {} + }; + } + + config_output[protocol].routers[routerName] = { + entryPoints: [`${protocol}-${port}`], + service: serviceName, + ...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {}) + }; + + config_output[protocol].services[serviceName] = { + loadBalancer: { + servers: targets + .filter((target: Target) => { + if (!target.enabled) { + return false; + } + if ( + site.type === "local" || + site.type === "wireguard" + ) { + if (!target.ip || !target.port) { + return false; + } + } else if (site.type === "newt") { + if (!target.internalPort || !site.subnet) { + return false; + } + } + return true; + }) + .map((target: Target) => { + if ( + site.type === "local" || + site.type === "wireguard" + ) { + return { + address: `${target.ip}:${target.port}` + }; + } else if (site.type === "newt") { + const ip = site.subnet!.split("/")[0]; + return { + address: `${ip}:${target.internalPort}` + }; + } + }), + ...(resource.stickySession + ? { + sticky: { + ipStrategy: { + depth: 0, + sourcePort: true + } + } + } + : {}) + } + }; + } + } + return config_output; +} From 2c8bf4f18c04c67f9193c6eca603a70e37597e47 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 16:23:24 -0700 Subject: [PATCH 101/219] Handle oss tls --- server/routers/traefik/getTraefikConfig.ts | 40 ++++++++++------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index da013878..89afee2c 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -5,6 +5,7 @@ import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import config from "@server/lib/config"; import { orgs, resources, sites, Target, targets } from "@server/db"; +import { build } from "@server/build"; let currentExitNodeId: number; @@ -52,12 +53,9 @@ export async function traefikConfigProvider( } } -export async function getTraefikConfig( - exitNodeId: number -): Promise { +export async function getTraefikConfig(exitNodeId: number): Promise { // Get all resources with related data const allResources = await db.transaction(async (tx) => { - // Get the site(s) on this exit node const resourcesWithRelations = await tx .select({ @@ -86,10 +84,7 @@ export async function getTraefikConfig( .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) .where( - or( - eq(sites.exitNodeId, exitNodeId), - isNull(sites.exitNodeId) - ) + or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)) ); // Get all resource IDs from the first query @@ -134,7 +129,7 @@ export async function getTraefikConfig( }); if (!allResources.length) { - return {} + return {}; } const badgerMiddlewareName = "badger"; @@ -236,18 +231,21 @@ export async function getTraefikConfig( preferWildcardCert = configDomain.prefer_wildcard_cert; } - const tls = { - certResolver: certResolver, - ...(preferWildcardCert - ? { - domains: [ - { - main: wildCard - } - ] - } - : {}) - }; + let tls = {}; + if (build == "oss") { + tls = { + certResolver: certResolver, + ...(preferWildcardCert + ? { + domains: [ + { + main: wildCard + } + ] + } + : {}) + }; + } const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; From dc50190dc3a091d984a309d27a089909469c1194 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 17:30:59 -0700 Subject: [PATCH 102/219] Handle token --- ...{hybridClientServer.ts => hybridServer.ts} | 12 +- server/index.ts | 2 +- server/lib/index.ts | 1 + server/lib/remoteProxy.ts | 2 + server/lib/remoteTraefikConfig.ts | 4 +- server/lib/tokenManager.ts | 209 ++++++++++++++++++ server/routers/ws/client.ts | 85 +------ 7 files changed, 232 insertions(+), 83 deletions(-) rename server/{hybridClientServer.ts => hybridServer.ts} (91%) create mode 100644 server/lib/tokenManager.ts diff --git a/server/hybridClientServer.ts b/server/hybridServer.ts similarity index 91% rename from server/hybridClientServer.ts rename to server/hybridServer.ts index 074fcd2e..7ce7efd7 100644 --- a/server/hybridClientServer.ts +++ b/server/hybridServer.ts @@ -3,10 +3,11 @@ import express from "express"; import { parse } from "url"; import logger from "@server/logger"; import config from "@server/lib/config"; -import { WebSocketClient, createWebSocketClient } from "./routers/ws/client"; +import { createWebSocketClient } from "./routers/ws/client"; import { addPeer, deletePeer } from "./routers/gerbil/peers"; import { db, exitNodes } from "./db"; import { TraefikConfigManager } from "./lib/remoteTraefikConfig"; +import { tokenManager } from "./lib/tokenManager"; export async function createHybridClientServer() { const monitor = new TraefikConfigManager(); @@ -21,11 +22,14 @@ export async function createHybridClientServer() { throw new Error("Hybrid configuration is not defined"); } + // Start the token manager + await tokenManager.start(); + + const token = await tokenManager.getToken(); + // Create client const client = createWebSocketClient( - "remoteExitNode", // or 'olm' - config.getRawConfig().hybrid!.id!, - config.getRawConfig().hybrid!.secret!, + token, config.getRawConfig().hybrid!.endpoint!, { reconnectInterval: 5000, diff --git a/server/index.ts b/server/index.ts index 42f85da6..7fd328c2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -7,7 +7,7 @@ import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db"; import { createIntegrationApiServer } from "./integrationApiServer"; -import { createHybridClientServer } from "./hybridClientServer"; +import { createHybridClientServer } from "./privateHybridServer.js"; import config from "@server/lib/config"; async function startServers() { diff --git a/server/lib/index.ts b/server/lib/index.ts index 9d2cfb1f..7705e0af 100644 --- a/server/lib/index.ts +++ b/server/lib/index.ts @@ -1 +1,2 @@ export * from "./response"; +export { tokenManager, TokenManager } from "./tokenManager"; diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts index 080c3bd3..e53f53f6 100644 --- a/server/lib/remoteProxy.ts +++ b/server/lib/remoteProxy.ts @@ -5,6 +5,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import config from "@server/lib/config"; +import { tokenManager } from "./tokenManager"; /** * Proxy function that forwards requests to the remote cloud server @@ -28,6 +29,7 @@ export const proxyToRemote = async ( data: req.body, headers: { 'Content-Type': 'application/json', + ...(await tokenManager.getAuthHeader()).headers }, params: req.query, timeout: 30000, // 30 second timeout diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index 08b2ab98..2e8ff529 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -5,6 +5,7 @@ import logger from "@server/logger"; import * as yaml from "js-yaml"; import axios from "axios"; import { db, exitNodes } from "@server/db"; +import { tokenManager } from "./tokenManager"; export class TraefikConfigManager { private intervalId: NodeJS.Timeout | null = null; @@ -162,7 +163,8 @@ export class TraefikConfigManager { } | null> { try { const resp = await axios.get( - `${config.getRawConfig().hybrid?.endpoint}/get-traefik-config` + `${config.getRawConfig().hybrid?.endpoint}/traefik-config`, + await tokenManager.getAuthHeader() ); if (resp.status !== 200) { diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts new file mode 100644 index 00000000..040dc609 --- /dev/null +++ b/server/lib/tokenManager.ts @@ -0,0 +1,209 @@ +import axios from "axios"; +import config from "@server/lib/config"; +import logger from "@server/logger"; + +export interface TokenResponse { + success: boolean; + message?: string; + data: { + token: string; + }; +} + +/** + * Token Manager - Handles automatic token refresh for hybrid server authentication + * + * Usage throughout the application: + * ```typescript + * import { tokenManager } from "@server/lib/tokenManager"; + * + * // Get the current valid token + * const token = await tokenManager.getToken(); + * + * // Force refresh if needed + * await tokenManager.refreshToken(); + * ``` + * + * The token manager automatically refreshes tokens every 24 hours by default + * and is started once in the privateHybridServer.ts file. + */ + +export class TokenManager { + private token: string | null = null; + private refreshInterval: NodeJS.Timeout | null = null; + private isRefreshing: boolean = false; + private refreshIntervalMs: number; + + constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000) { + // Default to 24 hours + this.refreshIntervalMs = refreshIntervalMs; + } + + /** + * Start the token manager - gets initial token and sets up refresh interval + */ + async start(): Promise { + try { + await this.refreshToken(); + this.setupRefreshInterval(); + logger.info("Token manager started successfully"); + } catch (error) { + logger.error("Failed to start token manager:", error); + throw error; + } + } + + /** + * Stop the token manager and clear refresh interval + */ + stop(): void { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + this.refreshInterval = null; + } + logger.info("Token manager stopped"); + } + + /** + * Get the current valid token + */ + async getToken(): Promise { + if (!this.token) { + if (this.isRefreshing) { + // Wait for current refresh to complete + await this.waitForRefresh(); + } else { + await this.refreshToken(); + } + } + + if (!this.token) { + throw new Error("No valid token available"); + } + + return this.token; + } + + async getAuthHeader() { + return { + headers: { + Authorization: `Bearer ${await this.getToken()}` + } + }; + } + + /** + * Force refresh the token + */ + async refreshToken(): Promise { + if (this.isRefreshing) { + await this.waitForRefresh(); + return; + } + + this.isRefreshing = true; + + try { + const hybridConfig = config.getRawConfig().hybrid; + + if ( + !hybridConfig?.id || + !hybridConfig?.secret || + !hybridConfig?.endpoint + ) { + throw new Error("Hybrid configuration is not defined"); + } + + const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`; + + const tokenData = { + remoteExitNodeId: hybridConfig.id, + secret: hybridConfig.secret + }; + + logger.debug("Requesting new token from server"); + + const response = await axios.post( + tokenEndpoint, + tokenData, + { + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": "x-csrf-protection" + }, + timeout: 10000 // 10 second timeout + } + ); + + if (!response.data.success) { + throw new Error( + `Failed to get token: ${response.data.message}` + ); + } + + if (!response.data.data.token) { + throw new Error("Received empty token from server"); + } + + this.token = response.data.data.token; + logger.debug("Token refreshed successfully"); + } catch (error) { + logger.error("Failed to refresh token:", error); + + if (axios.isAxiosError(error)) { + if (error.response) { + throw new Error( + `Failed to get token with status code: ${error.response.status}` + ); + } else if (error.request) { + throw new Error( + "Failed to request new token: No response received" + ); + } else { + throw new Error( + `Failed to request new token: ${error.message}` + ); + } + } else { + throw new Error(`Failed to get token: ${error}`); + } + } finally { + this.isRefreshing = false; + } + } + + /** + * Set up automatic token refresh interval + */ + private setupRefreshInterval(): void { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } + + this.refreshInterval = setInterval(async () => { + try { + logger.debug("Auto-refreshing token"); + await this.refreshToken(); + } catch (error) { + logger.error("Failed to auto-refresh token:", error); + } + }, this.refreshIntervalMs); + } + + /** + * Wait for current refresh operation to complete + */ + private async waitForRefresh(): Promise { + return new Promise((resolve) => { + const checkInterval = setInterval(() => { + if (!this.isRefreshing) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); + } +} + +// Export a singleton instance for use throughout the application +export const tokenManager = new TokenManager(); diff --git a/server/routers/ws/client.ts b/server/routers/ws/client.ts index 2cd5cfd7..3f1fbf54 100644 --- a/server/routers/ws/client.ts +++ b/server/routers/ws/client.ts @@ -14,14 +14,6 @@ export interface WSMessage { data: any; } -export interface TokenResponse { - success: boolean; - message?: string; - data: { - token: string; - }; -} - export type MessageHandler = (message: WSMessage) => void; export interface ClientOptions { @@ -33,45 +25,32 @@ export interface ClientOptions { export class WebSocketClient extends EventEmitter { private conn: WebSocket | null = null; - private config: Config; private baseURL: string; private handlers: Map = new Map(); private reconnectInterval: number; private isConnected: boolean = false; private pingInterval: number; private pingTimeout: number; - private clientType: string; private shouldReconnect: boolean = true; private reconnectTimer: NodeJS.Timeout | null = null; private pingTimer: NodeJS.Timeout | null = null; private pingTimeoutTimer: NodeJS.Timeout | null = null; + private token: string; constructor( - clientType: string, - id: string, - secret: string, + token: string, endpoint: string, options: ClientOptions = {} ) { super(); - this.clientType = clientType; - this.config = { - id, - secret, - endpoint - }; - + this.token = token; this.baseURL = options.baseURL || endpoint; this.reconnectInterval = options.reconnectInterval || 3000; this.pingInterval = options.pingInterval || 30000; this.pingTimeout = options.pingTimeout || 10000; } - public getConfig(): Config { - return this.config; - } - public async connect(): Promise { this.shouldReconnect = true; await this.connectWithRetry(); @@ -161,48 +140,6 @@ export class WebSocketClient extends EventEmitter { return this.isConnected; } - private async getToken(): Promise { - const baseURL = new URL(this.baseURL); - const tokenEndpoint = `${baseURL.origin}/api/v1/auth/${this.clientType}/get-token`; - - const tokenData = this.clientType === 'newt' - ? { newtId: this.config.id, secret: this.config.secret } - : { olmId: this.config.id, secret: this.config.secret }; - - try { - const response = await axios.post(tokenEndpoint, tokenData, { - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': 'x-csrf-protection' - }, - timeout: 10000 // 10 second timeout - }); - - if (!response.data.success) { - throw new Error(`Failed to get token: ${response.data.message}`); - } - - if (!response.data.data.token) { - throw new Error('Received empty token from server'); - } - - console.debug(`Received token: ${response.data.data.token}`); - return response.data.data.token; - } catch (error) { - if (axios.isAxiosError(error)) { - if (error.response) { - throw new Error(`Failed to get token with status code: ${error.response.status}`); - } else if (error.request) { - throw new Error('Failed to request new token: No response received'); - } else { - throw new Error(`Failed to request new token: ${error.message}`); - } - } else { - throw new Error(`Failed to get token: ${error}`); - } - } - } - private async connectWithRetry(): Promise { while (this.shouldReconnect) { try { @@ -221,18 +158,14 @@ export class WebSocketClient extends EventEmitter { } private async establishConnection(): Promise { - // Get token for authentication - const token = await this.getToken(); - this.emit('tokenUpdate', token); - // Parse the base URL to determine protocol and hostname const baseURL = new URL(this.baseURL); const wsProtocol = baseURL.protocol === 'https:' ? 'wss' : 'ws'; const wsURL = new URL(`${wsProtocol}://${baseURL.host}/api/v1/ws`); // Add token and client type to query parameters - wsURL.searchParams.set('token', token); - wsURL.searchParams.set('clientType', this.clientType); + wsURL.searchParams.set('token', this.token); + wsURL.searchParams.set('clientType', "remoteExitNode"); return new Promise((resolve, reject) => { const conn = new WebSocket(wsURL.toString()); @@ -330,13 +263,11 @@ export class WebSocketClient extends EventEmitter { // Factory function for easier instantiation export function createWebSocketClient( - clientType: string, - id: string, - secret: string, - endpoint: string, + token: string, + endpoint: string, options?: ClientOptions ): WebSocketClient { - return new WebSocketClient(clientType, id, secret, endpoint, options); + return new WebSocketClient(token, endpoint, options); } export default WebSocketClient; \ No newline at end of file From 285e24cdc7f0c3f300e52036d335783d9be0e9de Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 20:26:50 -0700 Subject: [PATCH 103/219] Use an epoch number for the clients online to fix query --- server/db/pg/schema.ts | 2 +- server/db/sqlite/schema.ts | 2 +- server/routers/olm/handleOlmPingMessage.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index d307f399..33d3fef0 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -513,7 +513,7 @@ export const clients = pgTable("clients", { megabytesIn: real("bytesIn"), megabytesOut: real("bytesOut"), lastBandwidthUpdate: varchar("lastBandwidthUpdate"), - lastPing: varchar("lastPing"), + lastPing: integer("lastPing"), type: varchar("type").notNull(), // "olm" online: boolean("online").notNull().default(false), // endpoint: varchar("endpoint"), diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 10f6686e..77136c68 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -213,7 +213,7 @@ export const clients = sqliteTable("clients", { megabytesIn: integer("bytesIn"), megabytesOut: integer("bytesOut"), lastBandwidthUpdate: text("lastBandwidthUpdate"), - lastPing: text("lastPing"), + lastPing: integer("lastPing"), type: text("type").notNull(), // "olm" online: integer("online", { mode: "boolean" }).notNull().default(false), // endpoint: text("endpoint"), diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index c95f36af..04659bb3 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -28,7 +28,7 @@ export const startOfflineChecker = (): void => { .set({ online: false }) .where( eq(clients.online, true) && - (lt(clients.lastPing, twoMinutesAgo.toISOString()) || isNull(clients.lastPing)) + (lt(clients.lastPing, twoMinutesAgo.getTime() / 1000) || isNull(clients.lastPing)) ); } catch (error) { @@ -72,7 +72,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { await db .update(clients) .set({ - lastPing: new Date().toISOString(), + lastPing: new Date().getTime() / 1000, online: true, }) .where(eq(clients.clientId, olm.clientId)); From b638adedff78efd23af2defe3defbc543ed7d5ba Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 20:27:48 -0700 Subject: [PATCH 104/219] Seperate get gerbil config --- server/hybridServer.ts | 4 ++ server/routers/gerbil/getConfig.ts | 84 +++++++++++++++++------------- server/routers/internal.ts | 7 ++- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index 7ce7efd7..f4e18b34 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -8,6 +8,7 @@ import { addPeer, deletePeer } from "./routers/gerbil/peers"; import { db, exitNodes } from "./db"; import { TraefikConfigManager } from "./lib/remoteTraefikConfig"; import { tokenManager } from "./lib/tokenManager"; +import { APP_VERSION } from "./lib/consts"; export async function createHybridClientServer() { const monitor = new TraefikConfigManager(); @@ -67,6 +68,9 @@ export async function createHybridClientServer() { // Listen to connection events client.on("connect", () => { console.log("Connected to WebSocket server"); + client.sendMessage("remoteExitNode/register", { + remoteExitNodeVersion: APP_VERSION + }); }); client.on("disconnect", () => { diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 696e7ea2..0a1c0f23 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { sites, resources, targets, exitNodes } from "@server/db"; +import { sites, resources, targets, exitNodes, ExitNode } from "@server/db"; import { db } from "@server/db"; import { eq, isNotNull, and } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; @@ -11,6 +11,7 @@ import { getUniqueExitNodeEndpointName } from "../../db/names"; import { findNextAvailableCidr } from "@server/lib/ip"; import { fromError } from "zod-validation-error"; import { getAllowedIps } from "../target/helpers"; +import { proxyToRemote } from "@server/lib/remoteProxy"; // Define Zod schema for request validation const getConfigSchema = z.object({ publicKey: z.string(), @@ -101,42 +102,12 @@ export async function getConfig( ); } - const sitesRes = await db - .select() - .from(sites) - .where( - and( - eq(sites.exitNodeId, exitNode[0].exitNodeId), - isNotNull(sites.pubKey), - isNotNull(sites.subnet) - ) - ); + // STOP HERE IN HYBRID MODE + if (config.isHybridMode()) { + return proxyToRemote(req, res, next, "gerbil/get-config"); + } - const peers = await Promise.all( - sitesRes.map(async (site) => { - if (site.type === "wireguard") { - return { - publicKey: site.pubKey, - allowedIps: await getAllowedIps(site.siteId) - }; - } else if (site.type === "newt") { - return { - publicKey: site.pubKey, - allowedIps: [site.subnet!] - }; - } - return { - publicKey: null, - allowedIps: [] - }; - }) - ); - - const configResponse: GetConfigResponse = { - listenPort: exitNode[0].listenPort || 51820, - ipAddress: exitNode[0].address, - peers - }; + const configResponse = await generateGerbilConfig(exitNode[0]); logger.debug("Sending config: ", configResponse); @@ -152,6 +123,47 @@ export async function getConfig( } } +async function generateGerbilConfig(exitNode: ExitNode) { + const sitesRes = await db + .select() + .from(sites) + .where( + and( + eq(sites.exitNodeId, exitNode.exitNodeId), + isNotNull(sites.pubKey), + isNotNull(sites.subnet) + ) + ); + + const peers = await Promise.all( + sitesRes.map(async (site) => { + if (site.type === "wireguard") { + return { + publicKey: site.pubKey, + allowedIps: await getAllowedIps(site.siteId) + }; + } else if (site.type === "newt") { + return { + publicKey: site.pubKey, + allowedIps: [site.subnet!] + }; + } + return { + publicKey: null, + allowedIps: [] + }; + }) + ); + + const configResponse: GetConfigResponse = { + listenPort: exitNode.listenPort || 51820, + ipAddress: exitNode.address, + peers + }; + + return configResponse; +} + async function getNextAvailableSubnet(): Promise { // Get all existing subnets from routes table const existingAddresses = await db diff --git a/server/routers/internal.ts b/server/routers/internal.ts index a84f6976..e91446dc 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -54,10 +54,6 @@ internalRouter.use("/gerbil", gerbilRouter); if (config.isHybridMode()) { // Use proxy router to forward requests to remote cloud server // Proxy endpoints for each gerbil route - gerbilRouter.post("/get-config", (req, res, next) => - proxyToRemote(req, res, next, "gerbil/get-config") - ); - gerbilRouter.post("/receive-bandwidth", (req, res, next) => proxyToRemote(req, res, next, "gerbil/receive-bandwidth") ); @@ -69,6 +65,9 @@ if (config.isHybridMode()) { gerbilRouter.post("/get-all-relays", (req, res, next) => proxyToRemote(req, res, next, "gerbil/get-all-relays") ); + + // GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER + // SO IT CAN REGISTER THE LOCAL EXIT NODE } else { // Use local gerbil endpoints gerbilRouter.post("/get-config", gerbil.getConfig); From eeb1d4954da72ab89617a6d1a3d66155f6d8997a Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 20:26:50 -0700 Subject: [PATCH 105/219] Use an epoch number for the clients online to fix query --- server/db/pg/schema.ts | 2 +- server/db/sqlite/schema.ts | 2 +- server/routers/olm/handleOlmPingMessage.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 477636f7..2ba10e3e 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -513,7 +513,7 @@ export const clients = pgTable("clients", { megabytesIn: real("bytesIn"), megabytesOut: real("bytesOut"), lastBandwidthUpdate: varchar("lastBandwidthUpdate"), - lastPing: varchar("lastPing"), + lastPing: integer("lastPing"), type: varchar("type").notNull(), // "olm" online: boolean("online").notNull().default(false), // endpoint: varchar("endpoint"), diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 460081a9..db1d8090 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -221,7 +221,7 @@ export const clients = sqliteTable("clients", { megabytesIn: integer("bytesIn"), megabytesOut: integer("bytesOut"), lastBandwidthUpdate: text("lastBandwidthUpdate"), - lastPing: text("lastPing"), + lastPing: integer("lastPing"), type: text("type").notNull(), // "olm" online: integer("online", { mode: "boolean" }).notNull().default(false), // endpoint: text("endpoint"), diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index c95f36af..04659bb3 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -28,7 +28,7 @@ export const startOfflineChecker = (): void => { .set({ online: false }) .where( eq(clients.online, true) && - (lt(clients.lastPing, twoMinutesAgo.toISOString()) || isNull(clients.lastPing)) + (lt(clients.lastPing, twoMinutesAgo.getTime() / 1000) || isNull(clients.lastPing)) ); } catch (error) { @@ -72,7 +72,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { await db .update(clients) .set({ - lastPing: new Date().toISOString(), + lastPing: new Date().getTime() / 1000, online: true, }) .where(eq(clients.clientId, olm.clientId)); From 34d705a54ef4d3f7e1b4d4ed74a6775473c89a6d Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 20:31:48 -0700 Subject: [PATCH 106/219] Rename olm offline --- server/routers/olm/handleOlmPingMessage.ts | 4 ++-- server/routers/ws/messageHandlers.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 04659bb3..2425c4d8 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -13,7 +13,7 @@ const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes * Starts the background interval that checks for clients that haven't pinged recently * and marks them as offline */ -export const startOfflineChecker = (): void => { +export const startOlmOfflineChecker = (): void => { if (offlineCheckerInterval) { return; // Already running } @@ -42,7 +42,7 @@ export const startOfflineChecker = (): void => { /** * Stops the background interval that checks for offline clients */ -export const stopOfflineChecker = (): void => { +export const stopOlmOfflineChecker = (): void => { if (offlineCheckerInterval) { clearInterval(offlineCheckerInterval); offlineCheckerInterval = null; diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index d85cc277..01889a8c 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -10,7 +10,7 @@ import { handleOlmRegisterMessage, handleOlmRelayMessage, handleOlmPingMessage, - startOfflineChecker + startOlmOfflineChecker } from "../olm"; import { MessageHandler } from "./ws"; @@ -26,4 +26,4 @@ export const messageHandlers: Record = { "newt/ping/request": handleNewtPingRequestMessage, }; -startOfflineChecker(); // this is to handle the offline check for olms +startOlmOfflineChecker(); // this is to handle the offline check for olms From b573d636480b94d989c33106958995d71c825da1 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 20:41:29 -0700 Subject: [PATCH 107/219] Add cols to exit node --- server/db/pg/schema.ts | 10 ++++++++-- server/db/sqlite/schema.ts | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 33d3fef0..8be65957 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -96,7 +96,7 @@ export const resources = pgTable("resources", { stickySession: boolean("stickySession").notNull().default(false), tlsServerName: varchar("tlsServerName"), setHostHeader: varchar("setHostHeader"), - enableProxy: boolean("enableProxy").default(true), + enableProxy: boolean("enableProxy").default(true) }); export const targets = pgTable("targets", { @@ -121,7 +121,13 @@ export const exitNodes = pgTable("exitNodes", { publicKey: varchar("publicKey").notNull(), listenPort: integer("listenPort").notNull(), reachableAt: varchar("reachableAt"), - maxConnections: integer("maxConnections") + maxConnections: integer("maxConnections"), + orgId: text("orgId").references(() => orgs.orgId, { + onDelete: "cascade" + }), + online: boolean("online").notNull().default(false), + lastPing: integer("lastPing"), + type: text("type").default("gerbil") // gerbil, remoteExitNode }); export const users = pgTable("user", { diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 77136c68..33442075 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -133,7 +133,13 @@ export const exitNodes = sqliteTable("exitNodes", { publicKey: text("publicKey").notNull(), listenPort: integer("listenPort").notNull(), reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control - maxConnections: integer("maxConnections") + maxConnections: integer("maxConnections"), + orgId: text("orgId").references(() => orgs.orgId, { + onDelete: "cascade" + }), + online: integer("online", { mode: "boolean" }).notNull().default(false), + lastPing: integer("lastPing"), + type: text("type").default("gerbil") // gerbil, remoteExitNode }); export const users = sqliteTable("user", { From 23079d9ac02e5edf02b1a818acf0b28b0ab2afb1 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 20:48:54 -0700 Subject: [PATCH 108/219] Fix exit node ping message --- server/hybridServer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index f4e18b34..64467d1b 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -89,5 +89,9 @@ export async function createHybridClientServer() { console.error("Failed to connect:", error); } - client.sendMessageInterval("heartbeat", { timestamp: Date.now() }, 10000); + client.sendMessageInterval( + "remoteExitNode/ping", + { timestamp: Date.now() / 1000 }, + 60000 + ); // send every minute } From ac87345b7a98f1320146fd5692e85f5a19955633 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 21:35:06 -0700 Subject: [PATCH 109/219] Seperate get relays --- server/routers/gerbil/getAllRelays.ts | 218 +++++++++++++++----------- 1 file changed, 125 insertions(+), 93 deletions(-) diff --git a/server/routers/gerbil/getAllRelays.ts b/server/routers/gerbil/getAllRelays.ts index a64fd78f..6eaf87e2 100644 --- a/server/routers/gerbil/getAllRelays.ts +++ b/server/routers/gerbil/getAllRelays.ts @@ -1,6 +1,15 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { clients, exitNodes, newts, olms, Site, sites, clientSites } from "@server/db"; +import { + clients, + exitNodes, + newts, + olms, + Site, + sites, + clientSites, + ExitNode +} from "@server/db"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; @@ -10,7 +19,7 @@ import { fromError } from "zod-validation-error"; // Define Zod schema for request validation const getAllRelaysSchema = z.object({ - publicKey: z.string().optional(), + publicKey: z.string().optional() }); // Type for peer destination @@ -44,103 +53,27 @@ export async function getAllRelays( const { publicKey } = parsedParams.data; if (!publicKey) { - return next(createHttpError(HttpCode.BAD_REQUEST, 'publicKey is required')); + return next( + createHttpError(HttpCode.BAD_REQUEST, "publicKey is required") + ); } // Fetch exit node - const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey)); + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.publicKey, publicKey)); if (!exitNode) { - return next(createHttpError(HttpCode.NOT_FOUND, "Exit node not found")); + return next( + createHttpError(HttpCode.NOT_FOUND, "Exit node not found") + ); } - // Fetch sites for this exit node - const sitesRes = await db.select().from(sites).where(eq(sites.exitNodeId, exitNode.exitNodeId)); + const mappings = await generateRelayMappings(exitNode); - if (sitesRes.length === 0) { - return res.status(HttpCode.OK).send({ - mappings: {} - }); - } - - // Initialize mappings object for multi-peer support - const mappings: { [key: string]: ProxyMapping } = {}; - - // Process each site - for (const site of sitesRes) { - if (!site.endpoint || !site.subnet || !site.listenPort) { - continue; - } - - // Find all clients associated with this site through clientSites - const clientSitesRes = await db - .select() - .from(clientSites) - .where(eq(clientSites.siteId, site.siteId)); - - for (const clientSite of clientSitesRes) { - if (!clientSite.endpoint) { - continue; - } - - // Add this site as a destination for the client - if (!mappings[clientSite.endpoint]) { - mappings[clientSite.endpoint] = { destinations: [] }; - } - - // Add site as a destination for this client - const destination: PeerDestination = { - destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort - }; - - // Check if this destination is already in the array to avoid duplicates - const isDuplicate = mappings[clientSite.endpoint].destinations.some( - dest => dest.destinationIP === destination.destinationIP && - dest.destinationPort === destination.destinationPort - ); - - if (!isDuplicate) { - mappings[clientSite.endpoint].destinations.push(destination); - } - } - - // Also handle site-to-site communication (all sites in the same org) - if (site.orgId) { - const orgSites = await db - .select() - .from(sites) - .where(eq(sites.orgId, site.orgId)); - - for (const peer of orgSites) { - // Skip self - if (peer.siteId === site.siteId || !peer.endpoint || !peer.subnet || !peer.listenPort) { - continue; - } - - // Add peer site as a destination for this site - if (!mappings[site.endpoint]) { - mappings[site.endpoint] = { destinations: [] }; - } - - const destination: PeerDestination = { - destinationIP: peer.subnet.split("/")[0], - destinationPort: peer.listenPort - }; - - // Check for duplicates - const isDuplicate = mappings[site.endpoint].destinations.some( - dest => dest.destinationIP === destination.destinationIP && - dest.destinationPort === destination.destinationPort - ); - - if (!isDuplicate) { - mappings[site.endpoint].destinations.push(destination); - } - } - } - } - - logger.debug(`Returning mappings for ${Object.keys(mappings).length} endpoints`); + logger.debug( + `Returning mappings for ${Object.keys(mappings).length} endpoints` + ); return res.status(HttpCode.OK).send({ mappings }); } catch (error) { logger.error(error); @@ -151,4 +84,103 @@ export async function getAllRelays( ) ); } -} \ No newline at end of file +} + +export async function generateRelayMappings(exitNode: ExitNode) { + // Fetch sites for this exit node + const sitesRes = await db + .select() + .from(sites) + .where(eq(sites.exitNodeId, exitNode.exitNodeId)); + + if (sitesRes.length === 0) { + return {}; + } + + // Initialize mappings object for multi-peer support + const mappings: { [key: string]: ProxyMapping } = {}; + + // Process each site + for (const site of sitesRes) { + if (!site.endpoint || !site.subnet || !site.listenPort) { + continue; + } + + // Find all clients associated with this site through clientSites + const clientSitesRes = await db + .select() + .from(clientSites) + .where(eq(clientSites.siteId, site.siteId)); + + for (const clientSite of clientSitesRes) { + if (!clientSite.endpoint) { + continue; + } + + // Add this site as a destination for the client + if (!mappings[clientSite.endpoint]) { + mappings[clientSite.endpoint] = { destinations: [] }; + } + + // Add site as a destination for this client + const destination: PeerDestination = { + destinationIP: site.subnet.split("/")[0], + destinationPort: site.listenPort + }; + + // Check if this destination is already in the array to avoid duplicates + const isDuplicate = mappings[clientSite.endpoint].destinations.some( + (dest) => + dest.destinationIP === destination.destinationIP && + dest.destinationPort === destination.destinationPort + ); + + if (!isDuplicate) { + mappings[clientSite.endpoint].destinations.push(destination); + } + } + + // Also handle site-to-site communication (all sites in the same org) + if (site.orgId) { + const orgSites = await db + .select() + .from(sites) + .where(eq(sites.orgId, site.orgId)); + + for (const peer of orgSites) { + // Skip self + if ( + peer.siteId === site.siteId || + !peer.endpoint || + !peer.subnet || + !peer.listenPort + ) { + continue; + } + + // Add peer site as a destination for this site + if (!mappings[site.endpoint]) { + mappings[site.endpoint] = { destinations: [] }; + } + + const destination: PeerDestination = { + destinationIP: peer.subnet.split("/")[0], + destinationPort: peer.listenPort + }; + + // Check for duplicates + const isDuplicate = mappings[site.endpoint].destinations.some( + (dest) => + dest.destinationIP === destination.destinationIP && + dest.destinationPort === destination.destinationPort + ); + + if (!isDuplicate) { + mappings[site.endpoint].destinations.push(destination); + } + } + } + } + + return mappings; +} From aaddde0a9bd5df89aa3645162a600b613f5b5f88 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 21:41:33 -0700 Subject: [PATCH 110/219] Add export --- server/routers/gerbil/getConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 0a1c0f23..4a6bcf05 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -123,7 +123,7 @@ export async function getConfig( } } -async function generateGerbilConfig(exitNode: ExitNode) { +export async function generateGerbilConfig(exitNode: ExitNode) { const sitesRes = await db .select() .from(sites) From 50cf28427391ee7128e781022d5135a9933c9aa5 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 21:45:44 -0700 Subject: [PATCH 111/219] Break out bandwidth --- server/routers/gerbil/receiveBandwidth.ts | 207 ++++++++++++---------- 1 file changed, 109 insertions(+), 98 deletions(-) diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index caadf7bb..350228ec 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -28,103 +28,7 @@ export const receiveBandwidth = async ( throw new Error("Invalid bandwidth data"); } - const currentTime = new Date(); - const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago - - // logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`); - - await db.transaction(async (trx) => { - // First, handle sites that are actively reporting bandwidth - const activePeers = bandwidthData.filter(peer => peer.bytesIn > 0); // Bytesout will have data as it tries to send keep alive messages - - if (activePeers.length > 0) { - // Remove any active peers from offline tracking since they're sending data - activePeers.forEach(peer => offlineSites.delete(peer.publicKey)); - - // Aggregate usage data by organization - const orgUsageMap = new Map(); - const orgUptimeMap = new Map(); - - // Update all active sites with bandwidth data and get the site data in one operation - const updatedSites = []; - for (const peer of activePeers) { - const updatedSite = await trx - .update(sites) - .set({ - megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`, - megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`, - lastBandwidthUpdate: currentTime.toISOString(), - online: true - }) - .where(eq(sites.pubKey, peer.publicKey)) - .returning({ - online: sites.online, - orgId: sites.orgId, - siteId: sites.siteId, - lastBandwidthUpdate: sites.lastBandwidthUpdate, - }); - - if (updatedSite.length > 0) { - updatedSites.push({ ...updatedSite[0], peer }); - } - } - - // Calculate org usage aggregations using the updated site data - for (const { peer, ...site } of updatedSites) { - // Aggregate bandwidth usage for the org - const totalBandwidth = peer.bytesIn + peer.bytesOut; - const currentOrgUsage = orgUsageMap.get(site.orgId) || 0; - orgUsageMap.set(site.orgId, currentOrgUsage + totalBandwidth); - - // Add 10 seconds of uptime for each active site - const currentOrgUptime = orgUptimeMap.get(site.orgId) || 0; - orgUptimeMap.set(site.orgId, currentOrgUptime + 10 / 60); // Store in minutes and jut add 10 seconds - } - } - - // Handle sites that reported zero bandwidth but need online status updated - const zeroBandwidthPeers = bandwidthData.filter(peer => - peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) // Bytesout will have data as it tries to send keep alive messages - ); - - if (zeroBandwidthPeers.length > 0) { - const zeroBandwidthSites = await trx - .select() - .from(sites) - .where(inArray(sites.pubKey, zeroBandwidthPeers.map(p => p.publicKey))); - - for (const site of zeroBandwidthSites) { - let newOnlineStatus = site.online; - - // Check if site should go offline based on last bandwidth update WITH DATA - if (site.lastBandwidthUpdate) { - const lastUpdateWithData = new Date(site.lastBandwidthUpdate); - if (lastUpdateWithData < oneMinuteAgo) { - newOnlineStatus = false; - } - } else { - // No previous data update recorded, set to offline - newOnlineStatus = false; - } - - // Always update lastBandwidthUpdate to show this instance is receiving reports - // Only update online status if it changed - if (site.online !== newOnlineStatus) { - await trx - .update(sites) - .set({ - online: newOnlineStatus - }) - .where(eq(sites.siteId, site.siteId)); - - // If site went offline, add it to our tracking set - if (!newOnlineStatus && site.pubKey) { - offlineSites.add(site.pubKey); - } - } - } - } - }); + await updateSiteBandwidth(bandwidthData); return response(res, { data: {}, @@ -142,4 +46,111 @@ export const receiveBandwidth = async ( ) ); } -}; \ No newline at end of file +}; + +export async function updateSiteBandwidth(bandwidthData: PeerBandwidth[]) { + const currentTime = new Date(); + const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago + + // logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`); + + await db.transaction(async (trx) => { + // First, handle sites that are actively reporting bandwidth + const activePeers = bandwidthData.filter((peer) => peer.bytesIn > 0); // Bytesout will have data as it tries to send keep alive messages + + if (activePeers.length > 0) { + // Remove any active peers from offline tracking since they're sending data + activePeers.forEach((peer) => offlineSites.delete(peer.publicKey)); + + // Aggregate usage data by organization + const orgUsageMap = new Map(); + const orgUptimeMap = new Map(); + + // Update all active sites with bandwidth data and get the site data in one operation + const updatedSites = []; + for (const peer of activePeers) { + const updatedSite = await trx + .update(sites) + .set({ + megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`, + megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`, + lastBandwidthUpdate: currentTime.toISOString(), + online: true + }) + .where(eq(sites.pubKey, peer.publicKey)) + .returning({ + online: sites.online, + orgId: sites.orgId, + siteId: sites.siteId, + lastBandwidthUpdate: sites.lastBandwidthUpdate + }); + + if (updatedSite.length > 0) { + updatedSites.push({ ...updatedSite[0], peer }); + } + } + + // Calculate org usage aggregations using the updated site data + for (const { peer, ...site } of updatedSites) { + // Aggregate bandwidth usage for the org + const totalBandwidth = peer.bytesIn + peer.bytesOut; + const currentOrgUsage = orgUsageMap.get(site.orgId) || 0; + orgUsageMap.set(site.orgId, currentOrgUsage + totalBandwidth); + + // Add 10 seconds of uptime for each active site + const currentOrgUptime = orgUptimeMap.get(site.orgId) || 0; + orgUptimeMap.set(site.orgId, currentOrgUptime + 10 / 60); // Store in minutes and jut add 10 seconds + } + } + + // Handle sites that reported zero bandwidth but need online status updated + const zeroBandwidthPeers = bandwidthData.filter( + (peer) => peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) // Bytesout will have data as it tries to send keep alive messages + ); + + if (zeroBandwidthPeers.length > 0) { + const zeroBandwidthSites = await trx + .select() + .from(sites) + .where( + inArray( + sites.pubKey, + zeroBandwidthPeers.map((p) => p.publicKey) + ) + ); + + for (const site of zeroBandwidthSites) { + let newOnlineStatus = site.online; + + // Check if site should go offline based on last bandwidth update WITH DATA + if (site.lastBandwidthUpdate) { + const lastUpdateWithData = new Date( + site.lastBandwidthUpdate + ); + if (lastUpdateWithData < oneMinuteAgo) { + newOnlineStatus = false; + } + } else { + // No previous data update recorded, set to offline + newOnlineStatus = false; + } + + // Always update lastBandwidthUpdate to show this instance is receiving reports + // Only update online status if it changed + if (site.online !== newOnlineStatus) { + await trx + .update(sites) + .set({ + online: newOnlineStatus + }) + .where(eq(sites.siteId, site.siteId)); + + // If site went offline, add it to our tracking set + if (!newOnlineStatus && site.pubKey) { + offlineSites.add(site.pubKey); + } + } + } + } + }); +} From 74d2527af56d87f2b4ed5fa72b2a066c144fd6a6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 13 Aug 2025 21:59:32 -0700 Subject: [PATCH 112/219] make email lower case in pangctl reset password closes #1210 --- cli/commands/resetUserSecurityKeys.ts | 25 +++++++++++++++---------- cli/commands/setAdminCredentials.ts | 3 ++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cli/commands/resetUserSecurityKeys.ts b/cli/commands/resetUserSecurityKeys.ts index 84af7cec..fdae0ebd 100644 --- a/cli/commands/resetUserSecurityKeys.ts +++ b/cli/commands/resetUserSecurityKeys.ts @@ -6,16 +6,19 @@ type ResetUserSecurityKeysArgs = { email: string; }; -export const resetUserSecurityKeys: CommandModule<{}, ResetUserSecurityKeysArgs> = { +export const resetUserSecurityKeys: CommandModule< + {}, + ResetUserSecurityKeysArgs +> = { command: "reset-user-security-keys", - describe: "Reset a user's security keys (passkeys) by deleting all their webauthn credentials", + describe: + "Reset a user's security keys (passkeys) by deleting all their webauthn credentials", builder: (yargs) => { - return yargs - .option("email", { - type: "string", - demandOption: true, - describe: "User email address" - }); + return yargs.option("email", { + type: "string", + demandOption: true, + describe: "User email address" + }); }, handler: async (argv: { email: string }) => { try { @@ -48,7 +51,9 @@ export const resetUserSecurityKeys: CommandModule<{}, ResetUserSecurityKeysArgs> process.exit(0); } - console.log(`Found ${userSecurityKeys.length} security key(s) for user '${email}'`); + console.log( + `Found ${userSecurityKeys.length} security key(s) for user '${email}'` + ); // Delete all security keys for the user await db @@ -64,4 +69,4 @@ export const resetUserSecurityKeys: CommandModule<{}, ResetUserSecurityKeysArgs> process.exit(1); } } -}; \ No newline at end of file +}; diff --git a/cli/commands/setAdminCredentials.ts b/cli/commands/setAdminCredentials.ts index 72ff8bff..c45da602 100644 --- a/cli/commands/setAdminCredentials.ts +++ b/cli/commands/setAdminCredentials.ts @@ -32,7 +32,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = { }, handler: async (argv: { email: string; password: string }) => { try { - const { email, password } = argv; + let { email, password } = argv; + email = email.trim().toLowerCase(); const parsed = passwordSchema.safeParse(password); From fcc86b07baeda7c19ea45cfacf16b677553ec5ab Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 22:05:26 -0700 Subject: [PATCH 113/219] Break out hole punch --- server/routers/gerbil/updateHolePunch.ts | 436 +++++++++++------------ 1 file changed, 217 insertions(+), 219 deletions(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 1d30b1ea..0eaa447e 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -66,228 +66,34 @@ export async function updateHolePunch( publicKey } = parsedParams.data; - let currentSiteId: number | undefined; - let destinations: PeerDestination[] = []; - - if (olmId) { - logger.debug( - `Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}${publicKey ? ` with exit node publicKey: ${publicKey}` : ""}` - ); - - const { session, olm: olmSession } = - await validateOlmSessionToken(token); - if (!session || !olmSession) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized") - ); - } - - if (olmId !== olmSession.olmId) { - logger.warn( - `Olm ID mismatch: ${olmId} !== ${olmSession.olmId}` - ); - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized") - ); - } - - const [olm] = await db + let exitNode: ExitNode | undefined; + if (publicKey) { + // Get the exit node by public key + [exitNode] = await db .select() - .from(olms) - .where(eq(olms.olmId, olmId)); - - if (!olm || !olm.clientId) { - logger.warn(`Olm not found: ${olmId}`); - return next( - createHttpError(HttpCode.NOT_FOUND, "Olm not found") - ); - } - - const [client] = await db - .update(clients) - .set({ - lastHolePunch: timestamp - }) - .where(eq(clients.clientId, olm.clientId)) - .returning(); - - let exitNode: ExitNode | undefined; - if (publicKey) { - // Get the exit node by public key - [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.publicKey, publicKey)); - } else { - // FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0 - [exitNode] = await db.select().from(exitNodes).limit(1); - } - - if (!exitNode) { - logger.warn(`Exit node not found for publicKey: ${publicKey}`); - return next( - createHttpError(HttpCode.NOT_FOUND, "Exit node not found") - ); - } - - // Get sites that are on this specific exit node and connected to this client - const sitesOnExitNode = await db - .select({ siteId: sites.siteId, subnet: sites.subnet, listenPort: sites.listenPort }) - .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) - .where( - and( - eq(sites.exitNodeId, exitNode.exitNodeId), - eq(clientSites.clientId, olm.clientId) - ) - ); - - // Update clientSites for each site on this exit node - for (const site of sitesOnExitNode) { - logger.debug( - `Updating site ${site.siteId} on exit node with publicKey: ${publicKey}` - ); - - await db - .update(clientSites) - .set({ - endpoint: `${ip}:${port}` - }) - .where( - and( - eq(clientSites.clientId, olm.clientId), - eq(clientSites.siteId, site.siteId) - ) - ); - } - - logger.debug( - `Updated ${sitesOnExitNode.length} sites on exit node with publicKey: ${publicKey}` - ); - if (!client) { - logger.warn(`Client not found for olm: ${olmId}`); - return next( - createHttpError(HttpCode.NOT_FOUND, "Client not found") - ); - } - - // Create a list of the destinations from the sites - for (const site of sitesOnExitNode) { - if (site.subnet && site.listenPort) { - destinations.push({ - destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort - }); - } - } - - } else if (newtId) { - logger.debug( - `Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}` - ); - - const { session, newt: newtSession } = - await validateNewtSessionToken(token); - - if (!session || !newtSession) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized") - ); - } - - if (newtId !== newtSession.newtId) { - logger.warn( - `Newt ID mismatch: ${newtId} !== ${newtSession.newtId}` - ); - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized") - ); - } - - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.newtId, newtId)); - - if (!newt || !newt.siteId) { - logger.warn(`Newt not found: ${newtId}`); - return next( - createHttpError(HttpCode.NOT_FOUND, "New not found") - ); - } - - currentSiteId = newt.siteId; - - // Update the current site with the new endpoint - const [updatedSite] = await db - .update(sites) - .set({ - endpoint: `${ip}:${port}`, - lastHolePunch: timestamp - }) - .where(eq(sites.siteId, newt.siteId)) - .returning(); - - if (!updatedSite || !updatedSite.subnet) { - logger.warn(`Site not found: ${newt.siteId}`); - return next( - createHttpError(HttpCode.NOT_FOUND, "Site not found") - ); - } - - // Find all clients that connect to this site - // const sitesClientPairs = await db - // .select() - // .from(clientSites) - // .where(eq(clientSites.siteId, newt.siteId)); - - // THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING - // Get client details for each client - // for (const pair of sitesClientPairs) { - // const [client] = await db - // .select() - // .from(clients) - // .where(eq(clients.clientId, pair.clientId)); - - // if (client && client.endpoint) { - // const [host, portStr] = client.endpoint.split(':'); - // if (host && portStr) { - // destinations.push({ - // destinationIP: host, - // destinationPort: parseInt(portStr, 10) - // }); - // } - // } - // } - - // If this is a newt/site, also add other sites in the same org - // if (updatedSite.orgId) { - // const orgSites = await db - // .select() - // .from(sites) - // .where(eq(sites.orgId, updatedSite.orgId)); - - // for (const site of orgSites) { - // // Don't add the current site to the destinations - // if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) { - // const [host, portStr] = site.endpoint.split(':'); - // if (host && portStr) { - // destinations.push({ - // destinationIP: host, - // destinationPort: site.listenPort - // }); - // } - // } - // } - // } + .from(exitNodes) + .where(eq(exitNodes.publicKey, publicKey)); + } else { + // FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0 + [exitNode] = await db.select().from(exitNodes).limit(1); } - // if (destinations.length === 0) { - // logger.warn( - // `No peer destinations found for olmId: ${olmId} or newtId: ${newtId}` - // ); - // return next(createHttpError(HttpCode.NOT_FOUND, "No peer destinations found")); - // } + if (!exitNode) { + logger.warn(`Exit node not found for publicKey: ${publicKey}`); + return next( + createHttpError(HttpCode.NOT_FOUND, "Exit node not found") + ); + } + + const destinations = await updateAndGenerateEndpointDestinations( + olmId, + newtId, + ip, + port, + timestamp, + token, + exitNode + ); logger.debug( `Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}` @@ -307,3 +113,195 @@ export async function updateHolePunch( ); } } + +export async function updateAndGenerateEndpointDestinations( + olmId: string | undefined, + newtId: string | undefined, + ip: string, + port: number, + timestamp: number, + token: string, + exitNode: ExitNode +) { + let currentSiteId: number | undefined; + let destinations: PeerDestination[] = []; + + if (olmId) { + logger.debug( + `Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}` + ); + + const { session, olm: olmSession } = + await validateOlmSessionToken(token); + if (!session || !olmSession) { + throw new Error("Unauthorized"); + } + + if (olmId !== olmSession.olmId) { + logger.warn(`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`); + throw new Error("Unauthorized"); + } + + const [olm] = await db.select().from(olms).where(eq(olms.olmId, olmId)); + + if (!olm || !olm.clientId) { + logger.warn(`Olm not found: ${olmId}`); + throw new Error("Olm not found"); + } + + const [client] = await db + .update(clients) + .set({ + lastHolePunch: timestamp + }) + .where(eq(clients.clientId, olm.clientId)) + .returning(); + + + + // Get sites that are on this specific exit node and connected to this client + const sitesOnExitNode = await db + .select({ + siteId: sites.siteId, + subnet: sites.subnet, + listenPort: sites.listenPort + }) + .from(sites) + .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) + .where( + and( + eq(sites.exitNodeId, exitNode.exitNodeId), + eq(clientSites.clientId, olm.clientId) + ) + ); + + // Update clientSites for each site on this exit node + for (const site of sitesOnExitNode) { + logger.debug( + `Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}` + ); + + await db + .update(clientSites) + .set({ + endpoint: `${ip}:${port}` + }) + .where( + and( + eq(clientSites.clientId, olm.clientId), + eq(clientSites.siteId, site.siteId) + ) + ); + } + + logger.debug( + `Updated ${sitesOnExitNode.length} sites on exit node ${exitNode.exitNodeId}` + ); + if (!client) { + logger.warn(`Client not found for olm: ${olmId}`); + throw new Error("Client not found"); + } + + // Create a list of the destinations from the sites + for (const site of sitesOnExitNode) { + if (site.subnet && site.listenPort) { + destinations.push({ + destinationIP: site.subnet.split("/")[0], + destinationPort: site.listenPort + }); + } + } + } else if (newtId) { + logger.debug( + `Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}` + ); + + const { session, newt: newtSession } = + await validateNewtSessionToken(token); + + if (!session || !newtSession) { + throw new Error("Unauthorized"); + } + + if (newtId !== newtSession.newtId) { + logger.warn( + `Newt ID mismatch: ${newtId} !== ${newtSession.newtId}` + ); + throw new Error("Unauthorized"); + } + + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.newtId, newtId)); + + if (!newt || !newt.siteId) { + logger.warn(`Newt not found: ${newtId}`); + throw new Error("Newt not found"); + } + + currentSiteId = newt.siteId; + + // Update the current site with the new endpoint + const [updatedSite] = await db + .update(sites) + .set({ + endpoint: `${ip}:${port}`, + lastHolePunch: timestamp + }) + .where(eq(sites.siteId, newt.siteId)) + .returning(); + + if (!updatedSite || !updatedSite.subnet) { + logger.warn(`Site not found: ${newt.siteId}`); + throw new Error("Site not found"); + } + + // Find all clients that connect to this site + // const sitesClientPairs = await db + // .select() + // .from(clientSites) + // .where(eq(clientSites.siteId, newt.siteId)); + + // THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING + // Get client details for each client + // for (const pair of sitesClientPairs) { + // const [client] = await db + // .select() + // .from(clients) + // .where(eq(clients.clientId, pair.clientId)); + + // if (client && client.endpoint) { + // const [host, portStr] = client.endpoint.split(':'); + // if (host && portStr) { + // destinations.push({ + // destinationIP: host, + // destinationPort: parseInt(portStr, 10) + // }); + // } + // } + // } + + // If this is a newt/site, also add other sites in the same org + // if (updatedSite.orgId) { + // const orgSites = await db + // .select() + // .from(sites) + // .where(eq(sites.orgId, updatedSite.orgId)); + + // for (const site of orgSites) { + // // Don't add the current site to the destinations + // if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) { + // const [host, portStr] = site.endpoint.split(':'); + // if (host && portStr) { + // destinations.push({ + // destinationIP: host, + // destinationPort: site.listenPort + // }); + // } + // } + // } + // } + } + return destinations; +} From d5a11edd0cc93cff22d42c47aacc4fce3a8f14df Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 10:38:22 -0700 Subject: [PATCH 114/219] Remove orgId --- server/db/pg/schema.ts | 3 --- server/db/sqlite/schema.ts | 3 --- server/lib/remoteTraefikConfig.ts | 3 ++- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 8be65957..50355abd 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -122,9 +122,6 @@ export const exitNodes = pgTable("exitNodes", { listenPort: integer("listenPort").notNull(), reachableAt: varchar("reachableAt"), maxConnections: integer("maxConnections"), - orgId: text("orgId").references(() => orgs.orgId, { - onDelete: "cascade" - }), online: boolean("online").notNull().default(false), lastPing: integer("lastPing"), type: text("type").default("gerbil") // gerbil, remoteExitNode diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 33442075..1ddf0f4c 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -134,9 +134,6 @@ export const exitNodes = sqliteTable("exitNodes", { listenPort: integer("listenPort").notNull(), reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control maxConnections: integer("maxConnections"), - orgId: text("orgId").references(() => orgs.orgId, { - onDelete: "cascade" - }), online: integer("online", { mode: "boolean" }).notNull().default(false), lastPing: integer("lastPing"), type: text("type").default("gerbil") // gerbil, remoteExitNode diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index 2e8ff529..9f1067e8 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -261,7 +261,8 @@ export class TraefikConfigManager { { params: { domains: domainArray - } + }, + headers: (await tokenManager.getAuthHeader()).headers } ); return response.data; From e5468a7391e425089ba637961fa4838c4cc6548a Mon Sep 17 00:00:00 2001 From: Jack Rosenberg Date: Thu, 14 Aug 2025 19:51:01 +0200 Subject: [PATCH 115/219] fix: change default integration_api to 3004 --- server/lib/readConfigFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 42fcefd3..8716c453 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -43,7 +43,7 @@ export const configSchema = z server: z.object({ integration_port: portSchema .optional() - .default(3003) + .default(3004) .transform(stoi) .pipe(portSchema.optional()), external_port: portSchema From aabfa91f801cdc3cecdbfb03365eb1d88f3b9798 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 11:11:01 -0700 Subject: [PATCH 116/219] Fix ping new integer --- server/index.ts | 2 +- server/routers/olm/handleOlmPingMessage.ts | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/server/index.ts b/server/index.ts index 7fd328c2..3e8c6769 100644 --- a/server/index.ts +++ b/server/index.ts @@ -7,7 +7,7 @@ import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db"; import { createIntegrationApiServer } from "./integrationApiServer"; -import { createHybridClientServer } from "./privateHybridServer.js"; +import { createHybridClientServer } from "./hybridServer"; import config from "@server/lib/config"; async function startServers() { diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 2425c4d8..6c4b5600 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -1,7 +1,7 @@ import { db } from "@server/db"; import { MessageHandler } from "../ws"; import { clients, Olm } from "@server/db"; -import { eq, lt, isNull } from "drizzle-orm"; +import { eq, lt, isNull, and, or } from "drizzle-orm"; import logger from "@server/logger"; // Track if the offline checker interval is running @@ -20,15 +20,20 @@ export const startOlmOfflineChecker = (): void => { offlineCheckerInterval = setInterval(async () => { try { - const twoMinutesAgo = new Date(Date.now() - OFFLINE_THRESHOLD_MS); + const twoMinutesAgo = Math.floor((Date.now() - OFFLINE_THRESHOLD_MS) / 1000); // Find clients that haven't pinged in the last 2 minutes and mark them as offline await db .update(clients) .set({ online: false }) .where( - eq(clients.online, true) && - (lt(clients.lastPing, twoMinutesAgo.getTime() / 1000) || isNull(clients.lastPing)) + and( + eq(clients.online, true), + or( + lt(clients.lastPing, twoMinutesAgo), + isNull(clients.lastPing) + ) + ) ); } catch (error) { @@ -72,7 +77,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { await db .update(clients) .set({ - lastPing: new Date().getTime() / 1000, + lastPing: Math.floor(Date.now() / 1000), online: true, }) .where(eq(clients.clientId, olm.clientId)); From 200e3af3844cd37baa3ee82ab288f371d4c7dd94 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 11:58:08 -0700 Subject: [PATCH 117/219] Websocket connects --- server/hybridServer.ts | 12 +++++++----- server/index.ts | 6 ++++-- server/lib/config.ts | 2 +- server/lib/tokenManager.ts | 2 ++ server/routers/external.ts | 2 +- server/routers/ws/client.ts | 28 +++++++++++++++++++++++----- 6 files changed, 38 insertions(+), 14 deletions(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index 64467d1b..7339db1c 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -11,6 +11,13 @@ import { tokenManager } from "./lib/tokenManager"; import { APP_VERSION } from "./lib/consts"; export async function createHybridClientServer() { + logger.info("Starting hybrid client server..."); + + // Start the token manager + await tokenManager.start(); + + const token = await tokenManager.getToken(); + const monitor = new TraefikConfigManager(); await monitor.start(); @@ -23,11 +30,6 @@ export async function createHybridClientServer() { throw new Error("Hybrid configuration is not defined"); } - // Start the token manager - await tokenManager.start(); - - const token = await tokenManager.getToken(); - // Create client const client = createWebSocketClient( token, diff --git a/server/index.ts b/server/index.ts index 3e8c6769..73f3ac90 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,11 +17,13 @@ async function startServers() { // Start all servers const apiServer = createApiServer(); const internalServer = createInternalServer(); - const nextServer = await createNextServer(); let hybridClientServer; + let nextServer; if (config.isHybridMode()) { - hybridClientServer = createHybridClientServer(); + hybridClientServer = await createHybridClientServer(); + } else { + nextServer = await createNextServer(); } let integrationServer; diff --git a/server/lib/config.ts b/server/lib/config.ts index c8c7b7c4..6b41df79 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -148,7 +148,7 @@ export class Config { } public isHybridMode() { - return this.rawConfig?.hybrid; + return typeof this.rawConfig?.hybrid === "object"; } public async checkSupporterKey() { diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts index 040dc609..8abfd969 100644 --- a/server/lib/tokenManager.ts +++ b/server/lib/tokenManager.ts @@ -67,6 +67,8 @@ export class TokenManager { /** * Get the current valid token */ + + // TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT async getToken(): Promise { if (!this.token) { if (this.isRefreshing) { diff --git a/server/routers/external.ts b/server/routers/external.ts index 5bae553e..776db454 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -848,7 +848,7 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 900, - keyGenerator: (req) => `newtGetToken:${req.body.newtId || req.ip}`, + keyGenerator: (req) => `olmGetToken:${req.body.newtId || req.ip}`, handler: (req, res, next) => { const message = `You can only request an Olm token ${900} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); diff --git a/server/routers/ws/client.ts b/server/routers/ws/client.ts index 3f1fbf54..c40c976f 100644 --- a/server/routers/ws/client.ts +++ b/server/routers/ws/client.ts @@ -36,6 +36,7 @@ export class WebSocketClient extends EventEmitter { private pingTimer: NodeJS.Timeout | null = null; private pingTimeoutTimer: NodeJS.Timeout | null = null; private token: string; + private isConnecting: boolean = false; constructor( token: string, @@ -46,14 +47,16 @@ export class WebSocketClient extends EventEmitter { this.token = token; this.baseURL = options.baseURL || endpoint; - this.reconnectInterval = options.reconnectInterval || 3000; + this.reconnectInterval = options.reconnectInterval || 5000; this.pingInterval = options.pingInterval || 30000; this.pingTimeout = options.pingTimeout || 10000; } public async connect(): Promise { this.shouldReconnect = true; - await this.connectWithRetry(); + if (!this.isConnecting) { + await this.connectWithRetry(); + } } public async close(): Promise { @@ -141,20 +144,30 @@ export class WebSocketClient extends EventEmitter { } private async connectWithRetry(): Promise { - while (this.shouldReconnect) { + if (this.isConnecting) return; + + this.isConnecting = true; + + while (this.shouldReconnect && !this.isConnected) { try { await this.establishConnection(); + this.isConnecting = false; return; } catch (error) { console.error(`Failed to connect: ${error}. Retrying in ${this.reconnectInterval}ms...`); - if (!this.shouldReconnect) return; + if (!this.shouldReconnect) { + this.isConnecting = false; + return; + } await new Promise(resolve => { this.reconnectTimer = setTimeout(resolve, this.reconnectInterval); }); } } + + this.isConnecting = false; } private async establishConnection(): Promise { @@ -174,6 +187,7 @@ export class WebSocketClient extends EventEmitter { console.debug('WebSocket connection established'); this.conn = conn; this.setConnected(true); + this.isConnecting = false; this.startPingMonitor(); this.emit('connect'); resolve(); @@ -232,6 +246,7 @@ export class WebSocketClient extends EventEmitter { private handleDisconnect(): void { this.setConnected(false); + this.isConnecting = false; // Clear ping timers if (this.pingTimer) { @@ -252,7 +267,10 @@ export class WebSocketClient extends EventEmitter { // Reconnect if needed if (this.shouldReconnect) { - this.connectWithRetry(); + // Add a small delay before starting reconnection to prevent immediate retry + setTimeout(() => { + this.connectWithRetry(); + }, 1000); } } From 65bdb232f410ad01e37274b99a9ae2826a920437 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 12:01:07 -0700 Subject: [PATCH 118/219] Use right logging --- server/hybridServer.ts | 10 +++++----- server/routers/ws/client.ts | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index 7339db1c..342cf8c0 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -69,26 +69,26 @@ export async function createHybridClientServer() { // Listen to connection events client.on("connect", () => { - console.log("Connected to WebSocket server"); + logger.info("Connected to WebSocket server"); client.sendMessage("remoteExitNode/register", { remoteExitNodeVersion: APP_VERSION }); }); client.on("disconnect", () => { - console.log("Disconnected from WebSocket server"); + logger.info("Disconnected from WebSocket server"); }); client.on("message", (message) => { - console.log("Received message:", message.type, message.data); + logger.info("Received message:", message.type, message.data); }); // Connect to the server try { await client.connect(); - console.log("Connection initiated"); + logger.info("Connection initiated"); } catch (error) { - console.error("Failed to connect:", error); + logger.error("Failed to connect:", error); } client.sendMessageInterval( diff --git a/server/routers/ws/client.ts b/server/routers/ws/client.ts index c40c976f..fda1e62c 100644 --- a/server/routers/ws/client.ts +++ b/server/routers/ws/client.ts @@ -2,6 +2,7 @@ import WebSocket from 'ws'; import axios from 'axios'; import { URL } from 'url'; import { EventEmitter } from 'events'; +import logger from '@server/logger'; export interface Config { id: string; @@ -96,7 +97,7 @@ export class WebSocketClient extends EventEmitter { data: data }; - console.debug(`Sending message: ${messageType}`, data); + logger.debug(`Sending message: ${messageType}`, data); this.conn.send(JSON.stringify(message), (error) => { if (error) { @@ -115,13 +116,13 @@ export class WebSocketClient extends EventEmitter { ): () => void { // Send immediately this.sendMessage(messageType, data).catch(err => { - console.error('Failed to send initial message:', err); + logger.error('Failed to send initial message:', err); }); // Set up interval const intervalId = setInterval(() => { this.sendMessage(messageType, data).catch(err => { - console.error('Failed to send message:', err); + logger.error('Failed to send message:', err); }); }, interval); @@ -154,7 +155,7 @@ export class WebSocketClient extends EventEmitter { this.isConnecting = false; return; } catch (error) { - console.error(`Failed to connect: ${error}. Retrying in ${this.reconnectInterval}ms...`); + logger.error(`Failed to connect: ${error}. Retrying in ${this.reconnectInterval}ms...`); if (!this.shouldReconnect) { this.isConnecting = false; @@ -184,7 +185,7 @@ export class WebSocketClient extends EventEmitter { const conn = new WebSocket(wsURL.toString()); conn.on('open', () => { - console.debug('WebSocket connection established'); + logger.debug('WebSocket connection established'); this.conn = conn; this.setConnected(true); this.isConnecting = false; @@ -202,17 +203,17 @@ export class WebSocketClient extends EventEmitter { } this.emit('message', message); } catch (error) { - console.error('Failed to parse message:', error); + logger.error('Failed to parse message:', error); } }); conn.on('close', (code, reason) => { - console.debug(`WebSocket connection closed: ${code} ${reason}`); + logger.debug(`WebSocket connection closed: ${code} ${reason}`); this.handleDisconnect(); }); conn.on('error', (error) => { - console.error('WebSocket error:', error); + logger.error('WebSocket error:', error); if (this.conn === null) { // Connection failed during establishment reject(error); @@ -237,7 +238,7 @@ export class WebSocketClient extends EventEmitter { // Set timeout for pong response this.pingTimeoutTimer = setTimeout(() => { - console.error('Ping timeout - no pong received'); + logger.error('Ping timeout - no pong received'); this.handleDisconnect(); }, this.pingTimeout); } From f7b82f0a7a43d4952a1897b795759f8fc416e77b Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 12:35:33 -0700 Subject: [PATCH 119/219] Work on pulling in remote traefik --- .gitignore | 2 + server/hybridServer.ts | 2 +- server/lib/remoteTraefikConfig.ts | 77 +++++++++++++++++++++++++------ 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 167b4a91..2f1749ef 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ bin test_event.json .idea/ server/db/index.ts +dynamic/ +certificates/ diff --git a/server/hybridServer.ts b/server/hybridServer.ts index 342cf8c0..d5a61a9e 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -80,7 +80,7 @@ export async function createHybridClientServer() { }); client.on("message", (message) => { - logger.info("Received message:", message.type, message.data); + logger.info(`Received message: ${message.type} ${JSON.stringify(message.data)}`); }); // Connect to the server diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index 9f1067e8..fba86c12 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -87,17 +87,17 @@ export class TraefikConfigManager { public async HandleTraefikConfig(): Promise { try { // Get all active domains for this exit node via HTTP call - const getActiveDomainsFromTraefik = - await this.getActiveDomainsFromTraefik(); + const getTraefikConfig = + await this.getTraefikConfig(); - if (!getActiveDomainsFromTraefik) { + if (!getTraefikConfig) { logger.error( "Failed to fetch active domains from traefik config" ); return; } - const { domains, traefikConfig } = getActiveDomainsFromTraefik; + const { domains, traefikConfig } = getTraefikConfig; // Add static domains from config // const staticDomains = [config.getRawConfig().app.dashboard_url]; @@ -150,31 +150,32 @@ export class TraefikConfigManager { // Update active domains tracking this.activeDomains = domains; } catch (error) { - logger.error("Error in certificate monitoring cycle:", error); + logger.error("Error in traefik config monitoring cycle:", error); } } /** * Get all domains currently in use from traefik config API */ - private async getActiveDomainsFromTraefik(): Promise<{ + private async getTraefikConfig(): Promise<{ domains: Set; traefikConfig: any; } | null> { try { const resp = await axios.get( - `${config.getRawConfig().hybrid?.endpoint}/traefik-config`, + `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/traefik-config`, await tokenManager.getAuthHeader() ); if (resp.status !== 200) { logger.error( - `Failed to fetch traefik config: ${resp.status} ${resp.statusText}` + `Failed to fetch traefik config: ${resp.status} ${resp.statusText}`, + { responseData: resp.data } ); return null; } - const traefikConfig = resp.data; + const traefikConfig = resp.data.data; const domains = new Set(); if (traefikConfig?.http?.routers) { @@ -190,9 +191,29 @@ export class TraefikConfigManager { } } } + + logger.debug( + `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` + ); + return { domains, traefikConfig }; } catch (err) { - logger.error("Failed to fetch traefik config:", err); + // Extract useful information from axios error without circular references + if (err && typeof err === 'object' && 'response' in err) { + const axiosError = err as any; + logger.error("Failed to fetch traefik config:", { + status: axiosError.response?.status, + statusText: axiosError.response?.statusText, + data: axiosError.response?.data, + message: axiosError.message, + url: axiosError.config?.url + }); + } else { + logger.error("Failed to fetch traefik config:", { + message: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined + }); + } return null; } } @@ -257,7 +278,7 @@ export class TraefikConfigManager { try { const response = await axios.get( - `${config.getRawConfig().hybrid?.endpoint}/certificates/domains`, + `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/certificates/domains`, { params: { domains: domainArray @@ -265,9 +286,39 @@ export class TraefikConfigManager { headers: (await tokenManager.getAuthHeader()).headers } ); - return response.data; + + if (response.status !== 200) { + logger.error( + `Failed to fetch certificates for domains: ${response.status} ${response.statusText}`, + { responseData: response.data, domains: domainArray } + ); + return []; + } + + logger.debug( + `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains` + ); + + return response.data.data; } catch (error) { - console.error("Error fetching resource by domain:", error); + // Extract useful information from axios error without circular references + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as any; + logger.error("Error fetching certificates for domains:", { + status: axiosError.response?.status, + statusText: axiosError.response?.statusText, + data: axiosError.response?.data, + message: axiosError.message, + url: axiosError.config?.url, + domains: domainArray + }); + } else { + logger.error("Error fetching certificates for domains:", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + domains: domainArray + }); + } return []; } } From 6600de732008fc33d6aba40773fbf8350e41fd3f Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 14:47:07 -0700 Subject: [PATCH 120/219] Traefik config & gerbil config working? --- server/hybridServer.ts | 8 -------- server/lib/remoteProxy.ts | 2 ++ server/lib/remoteTraefikConfig.ts | 2 ++ server/routers/gerbil/getConfig.ts | 2 +- server/routers/internal.ts | 11 +++++++---- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index d5a61a9e..22ba64c8 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -22,14 +22,6 @@ export async function createHybridClientServer() { await monitor.start(); - if ( - !config.getRawConfig().hybrid?.id || - !config.getRawConfig().hybrid?.secret || - !config.getRawConfig().hybrid?.endpoint - ) { - throw new Error("Hybrid configuration is not defined"); - } - // Create client const client = createWebSocketClient( token, diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts index e53f53f6..3b9dcd69 100644 --- a/server/lib/remoteProxy.ts +++ b/server/lib/remoteProxy.ts @@ -36,6 +36,8 @@ export const proxyToRemote = async ( validateStatus: () => true // Don't throw on non-2xx status codes }); + logger.debug(`Proxy response: ${JSON.stringify(response.data)}`); + // Forward the response status and data return res.status(response.status).json(response.data); diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index fba86c12..36bdad24 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -120,6 +120,8 @@ export class TraefikConfigManager { const validCertificates = await this.getValidCertificatesForDomains(domains); + // logger.debug(`Valid certs array: ${JSON.stringify(validCertificates)}`); + // Download and decrypt new certificates await this.processValidCertificates(validCertificates); diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 4a6bcf05..d8f4c56e 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -104,7 +104,7 @@ export async function getConfig( // STOP HERE IN HYBRID MODE if (config.isHybridMode()) { - return proxyToRemote(req, res, next, "gerbil/get-config"); + return proxyToRemote(req, res, next, "hybrid/gerbil/get-config"); } const configResponse = await generateGerbilConfig(exitNode[0]); diff --git a/server/routers/internal.ts b/server/routers/internal.ts index e91446dc..977248e5 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -55,27 +55,30 @@ if (config.isHybridMode()) { // Use proxy router to forward requests to remote cloud server // Proxy endpoints for each gerbil route gerbilRouter.post("/receive-bandwidth", (req, res, next) => - proxyToRemote(req, res, next, "gerbil/receive-bandwidth") + proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth") ); gerbilRouter.post("/update-hole-punch", (req, res, next) => - proxyToRemote(req, res, next, "gerbil/update-hole-punch") + proxyToRemote(req, res, next, "hybrid/gerbil/update-hole-punch") ); gerbilRouter.post("/get-all-relays", (req, res, next) => - proxyToRemote(req, res, next, "gerbil/get-all-relays") + proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays") ); // GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER // SO IT CAN REGISTER THE LOCAL EXIT NODE } else { // Use local gerbil endpoints - gerbilRouter.post("/get-config", gerbil.getConfig); gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); } +// WE HANDLE THE PROXY INSIDE OF THIS FUNCTION +// SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL +gerbilRouter.post("/get-config", gerbil.getConfig); + // Badger routes const badgerRouter = Router(); internalRouter.use("/badger", badgerRouter); From 04ecf41c5ac7620415d5eb9e956cde813563803f Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 15:39:05 -0700 Subject: [PATCH 121/219] Move exit node comms to new file --- server/hybridServer.ts | 76 ++++++- server/lib/exitNodeComms.ts | 86 ++++++++ server/routers/client/updateClient.ts | 202 +++++++++--------- server/routers/gerbil/peers.ts | 65 ++---- server/routers/newt/handleGetConfigMessage.ts | 63 +++--- 5 files changed, 295 insertions(+), 197 deletions(-) create mode 100644 server/lib/exitNodeComms.ts diff --git a/server/hybridServer.ts b/server/hybridServer.ts index 22ba64c8..adf9ce25 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -1,6 +1,3 @@ -import next from "next"; -import express from "express"; -import { parse } from "url"; import logger from "@server/logger"; import config from "@server/lib/config"; import { createWebSocketClient } from "./routers/ws/client"; @@ -9,6 +6,7 @@ import { db, exitNodes } from "./db"; import { TraefikConfigManager } from "./lib/remoteTraefikConfig"; import { tokenManager } from "./lib/tokenManager"; import { APP_VERSION } from "./lib/consts"; +import axios from "axios"; export async function createHybridClientServer() { logger.info("Starting hybrid client server..."); @@ -34,7 +32,7 @@ export async function createHybridClientServer() { ); // Register message handlers - client.registerHandler("remote/peers/add", async (message) => { + client.registerHandler("remoteExitNode/peers/add", async (message) => { const { pubKey, allowedIps } = message.data; // TODO: we are getting the exit node twice here @@ -46,7 +44,7 @@ export async function createHybridClientServer() { }); }); - client.registerHandler("remote/peers/remove", async (message) => { + client.registerHandler("remoteExitNode/peers/remove", async (message) => { const { pubKey } = message.data; // TODO: we are getting the exit node twice here @@ -55,7 +53,69 @@ export async function createHybridClientServer() { await deletePeer(exitNode.exitNodeId, pubKey); }); - client.registerHandler("remote/traefik/reload", async (message) => { + // /update-proxy-mapping + client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => { + try { + const [exitNode] = await db.select().from(exitNodes).limit(1); + if (!exitNode) { + logger.error("No exit node found for proxy mapping update"); + return; + } + + const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data); + logger.info(`Successfully updated proxy mapping: ${response.status}`); + } catch (error) { + // Extract useful information from axios error without circular references + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as any; + logger.error("Failed to update proxy mapping:", { + status: axiosError.response?.status, + statusText: axiosError.response?.statusText, + data: axiosError.response?.data, + message: axiosError.message, + url: axiosError.config?.url + }); + } else { + logger.error("Failed to update proxy mapping:", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }); + } + } + }); + + // /update-destinations + client.registerHandler("remoteExitNode/update-destinations", async (message) => { + try { + const [exitNode] = await db.select().from(exitNodes).limit(1); + if (!exitNode) { + logger.error("No exit node found for destinations update"); + return; + } + + const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data); + logger.info(`Successfully updated destinations: ${response.status}`); + } catch (error) { + // Extract useful information from axios error without circular references + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as any; + logger.error("Failed to update destinations:", { + status: axiosError.response?.status, + statusText: axiosError.response?.statusText, + data: axiosError.response?.data, + message: axiosError.message, + url: axiosError.config?.url + }); + } else { + logger.error("Failed to update proxy mapping:", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }); + } + } + }); + + client.registerHandler("remoteExitNode/traefik/reload", async (message) => { await monitor.HandleTraefikConfig(); }); @@ -72,7 +132,9 @@ export async function createHybridClientServer() { }); client.on("message", (message) => { - logger.info(`Received message: ${message.type} ${JSON.stringify(message.data)}`); + logger.info( + `Received message: ${message.type} ${JSON.stringify(message.data)}` + ); }); // Connect to the server diff --git a/server/lib/exitNodeComms.ts b/server/lib/exitNodeComms.ts new file mode 100644 index 00000000..f79b718f --- /dev/null +++ b/server/lib/exitNodeComms.ts @@ -0,0 +1,86 @@ +import axios from "axios"; +import logger from "@server/logger"; +import { ExitNode } from "@server/db"; + +interface ExitNodeRequest { + remoteType: string; + localPath: string; + method?: "POST" | "DELETE" | "GET" | "PUT"; + data?: any; + queryParams?: Record; +} + +/** + * Sends a request to an exit node, handling both remote and local exit nodes + * @param exitNode The exit node to send the request to + * @param request The request configuration + * @returns Promise Response data for local nodes, undefined for remote nodes + */ +export async function sendToExitNode( + exitNode: ExitNode, + request: ExitNodeRequest +): Promise { + if (!exitNode.reachableAt) { + throw new Error( + `Exit node with ID ${exitNode.exitNodeId} is not reachable` + ); + } + + // Handle local exit node with HTTP API + const method = request.method || "POST"; + let url = `${exitNode.reachableAt}${request.localPath}`; + + // Add query parameters if provided + if (request.queryParams) { + const params = new URLSearchParams(request.queryParams); + url += `?${params.toString()}`; + } + + try { + let response; + + switch (method) { + case "POST": + response = await axios.post(url, request.data, { + headers: { + "Content-Type": "application/json" + } + }); + break; + case "DELETE": + response = await axios.delete(url); + break; + case "GET": + response = await axios.get(url); + break; + case "PUT": + response = await axios.put(url, request.data, { + headers: { + "Content-Type": "application/json" + } + }); + break; + default: + throw new Error(`Unsupported HTTP method: ${method}`); + } + + logger.info(`Exit node request successful:`, { + method, + url, + status: response.data.status + }); + + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error( + `Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` + ); + } else { + logger.error( + `Error making ${method} request for exit node at ${exitNode.reachableAt}: ${error}` + ); + } + throw error; + } +} diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index de4a7b5e..81ee4278 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -17,7 +17,7 @@ import { addPeer as olmAddPeer, deletePeer as olmDeletePeer } from "../olm/peers"; -import axios from "axios"; +import { sendToExitNode } from "../../lib/exitNodeComms"; const updateClientParamsSchema = z .object({ @@ -141,13 +141,15 @@ export async function updateClient( const isRelayed = true; // get the clientsite - const [clientSite] = await db + const [clientSite] = await db .select() .from(clientSites) - .where(and( - eq(clientSites.clientId, client.clientId), - eq(clientSites.siteId, siteId) - )) + .where( + and( + eq(clientSites.clientId, client.clientId), + eq(clientSites.siteId, siteId) + ) + ) .limit(1); if (!clientSite || !clientSite.endpoint) { @@ -158,7 +160,7 @@ export async function updateClient( const site = await newtAddPeer(siteId, { publicKey: client.pubKey, allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: isRelayed ? "" : clientSite.endpoint + endpoint: isRelayed ? "" : clientSite.endpoint }); if (!site) { @@ -270,114 +272,102 @@ export async function updateClient( } } - // get all sites for this client and join with exit nodes with site.exitNodeId - const sitesData = await db - .select() - .from(sites) - .innerJoin( - clientSites, - eq(sites.siteId, clientSites.siteId) - ) - .leftJoin( - exitNodes, - eq(sites.exitNodeId, exitNodes.exitNodeId) - ) - .where(eq(clientSites.clientId, client.clientId)); + // get all sites for this client and join with exit nodes with site.exitNodeId + const sitesData = await db + .select() + .from(sites) + .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) + .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) + .where(eq(clientSites.clientId, client.clientId)); - let exitNodeDestinations: { - reachableAt: string; - sourceIp: string; - sourcePort: number; - destinations: PeerDestination[]; - }[] = []; + let exitNodeDestinations: { + reachableAt: string; + exitNodeId: number; + type: string; + sourceIp: string; + sourcePort: number; + destinations: PeerDestination[]; + }[] = []; - for (const site of sitesData) { - if (!site.sites.subnet) { - logger.warn( - `Site ${site.sites.siteId} has no subnet, skipping` - ); - continue; - } - - if (!site.clientSites.endpoint) { - logger.warn( - `Site ${site.sites.siteId} has no endpoint, skipping` - ); - continue; - } - - // find the destinations in the array - let destinations = exitNodeDestinations.find( - (d) => d.reachableAt === site.exitNodes?.reachableAt + for (const site of sitesData) { + if (!site.sites.subnet) { + logger.warn( + `Site ${site.sites.siteId} has no subnet, skipping` ); - - if (!destinations) { - destinations = { - reachableAt: site.exitNodes?.reachableAt || "", - sourceIp: site.clientSites.endpoint.split(":")[0] || "", - sourcePort: parseInt(site.clientSites.endpoint.split(":")[1]) || 0, - destinations: [ - { - destinationIP: - site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 - } - ] - }; - } else { - // add to the existing destinations - destinations.destinations.push({ - destinationIP: site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 - }); - } - - // update it in the array - exitNodeDestinations = exitNodeDestinations.filter( - (d) => d.reachableAt !== site.exitNodes?.reachableAt - ); - exitNodeDestinations.push(destinations); + continue; } - for (const destination of exitNodeDestinations) { - try { - logger.info( - `Updating destinations for exit node at ${destination.reachableAt}` - ); - const payload = { - sourceIp: destination.sourceIp, - sourcePort: destination.sourcePort, - destinations: destination.destinations - }; - logger.info( - `Payload for update-destinations: ${JSON.stringify(payload, null, 2)}` - ); - const response = await axios.post( - `${destination.reachableAt}/update-destinations`, - payload, + if (!site.clientSites.endpoint) { + logger.warn( + `Site ${site.sites.siteId} has no endpoint, skipping` + ); + continue; + } + + // find the destinations in the array + let destinations = exitNodeDestinations.find( + (d) => d.reachableAt === site.exitNodes?.reachableAt + ); + + if (!destinations) { + destinations = { + reachableAt: site.exitNodes?.reachableAt || "", + exitNodeId: site.exitNodes?.exitNodeId || 0, + type: site.exitNodes?.type || "", + sourceIp: site.clientSites.endpoint.split(":")[0] || "", + sourcePort: + parseInt(site.clientSites.endpoint.split(":")[1]) || + 0, + destinations: [ { - headers: { - "Content-Type": "application/json" - } + destinationIP: site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 } - ); - - logger.info("Destinations updated:", { - peer: response.data.status - }); - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error( - `Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}` - ); - } else { - logger.error( - `Error updating destinations for exit node at ${destination.reachableAt}: ${error}` - ); - } - } + ] + }; + } else { + // add to the existing destinations + destinations.destinations.push({ + destinationIP: site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 + }); } + // update it in the array + exitNodeDestinations = exitNodeDestinations.filter( + (d) => d.reachableAt !== site.exitNodes?.reachableAt + ); + exitNodeDestinations.push(destinations); + } + + for (const destination of exitNodeDestinations) { + logger.info( + `Updating destinations for exit node at ${destination.reachableAt}` + ); + const payload = { + sourceIp: destination.sourceIp, + sourcePort: destination.sourcePort, + destinations: destination.destinations + }; + logger.info( + `Payload for update-destinations: ${JSON.stringify(payload, null, 2)}` + ); + + // Create an ExitNode-like object for sendToExitNode + const exitNodeForComm = { + exitNodeId: destination.exitNodeId, + type: destination.type, + reachableAt: destination.reachableAt + } as any; // Using 'as any' since we know sendToExitNode will handle this correctly + + await sendToExitNode(exitNodeForComm, { + remoteType: "remoteExitNode/update-destinations", + localPath: "/update-destinations", + method: "POST", + data: payload + }); + } + // Fetch the updated client const [updatedClient] = await trx .select() diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts index 40203c41..51a338a7 100644 --- a/server/routers/gerbil/peers.ts +++ b/server/routers/gerbil/peers.ts @@ -1,8 +1,8 @@ -import axios from "axios"; import logger from "@server/logger"; import { db } from "@server/db"; import { exitNodes } from "@server/db"; import { eq } from "drizzle-orm"; +import { sendToExitNode } from "../../lib/exitNodeComms"; export async function addPeer( exitNodeId: number, @@ -22,34 +22,13 @@ export async function addPeer( if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); } - if (!exitNode.reachableAt) { - throw new Error(`Exit node with ID ${exitNodeId} is not reachable`); - } - try { - const response = await axios.post( - `${exitNode.reachableAt}/peer`, - peer, - { - headers: { - "Content-Type": "application/json" - } - } - ); - - logger.info("Peer added successfully:", { peer: response.data.status }); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error( - `Error adding peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` - ); - } else { - logger.error( - `Error adding peer for exit node at ${exitNode.reachableAt}: ${error}` - ); - } - } + return await sendToExitNode(exitNode, { + remoteType: "remoteExitNode/peers/add", + localPath: "/peer", + method: "POST", + data: peer + }); } export async function deletePeer(exitNodeId: number, publicKey: string) { @@ -64,24 +43,16 @@ export async function deletePeer(exitNodeId: number, publicKey: string) { if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); } - if (!exitNode.reachableAt) { - throw new Error(`Exit node with ID ${exitNodeId} is not reachable`); - } - try { - const response = await axios.delete( - `${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}` - ); - logger.info("Peer deleted successfully:", response.data.status); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error( - `Error deleting peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` - ); - } else { - logger.error( - `Error deleting peer for exit node at ${exitNode.reachableAt}: ${error}` - ); + + return await sendToExitNode(exitNode, { + remoteType: "remoteExitNode/peers/remove", + localPath: "/peer", + method: "DELETE", + data: { + publicKey: publicKey + }, + queryParams: { + public_key: publicKey } - } + }); } diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index b2594a71..6142cb05 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -13,7 +13,7 @@ import { import { clients, clientSites, Newt, sites } from "@server/db"; import { eq, and, inArray } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; -import axios from "axios"; +import { sendToExitNode } from "../../lib/exitNodeComms"; const inputSchema = z.object({ publicKey: z.string(), @@ -102,41 +102,28 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { .from(exitNodes) .where(eq(exitNodes.exitNodeId, site.exitNodeId)) .limit(1); - if (exitNode.reachableAt && existingSite.subnet && existingSite.listenPort) { - try { - const response = await axios.post( - `${exitNode.reachableAt}/update-proxy-mapping`, - { - oldDestination: { - destinationIP: existingSite.subnet?.split("/")[0], - destinationPort: existingSite.listenPort - }, - newDestination: { - destinationIP: site.subnet?.split("/")[0], - destinationPort: site.listenPort - } - }, - { - headers: { - "Content-Type": "application/json" - } - } - ); - - logger.info("Destinations updated:", { - peer: response.data.status - }); - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error( - `Error updating proxy mapping (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` - ); - } else { - logger.error( - `Error updating proxy mapping for exit node at ${exitNode.reachableAt}: ${error}` - ); + if ( + exitNode.reachableAt && + existingSite.subnet && + existingSite.listenPort + ) { + const payload = { + oldDestination: { + destinationIP: existingSite.subnet?.split("/")[0], + destinationPort: existingSite.listenPort + }, + newDestination: { + destinationIP: site.subnet?.split("/")[0], + destinationPort: site.listenPort } - } + }; + + await sendToExitNode(exitNode, { + remoteType: "remoteExitNode/update-proxy-mapping", + localPath: "/update-proxy-mapping", + method: "POST", + data: payload + }); } } @@ -237,7 +224,9 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { protocol: resources.protocol }) .from(resources) - .where(and(eq(resources.siteId, siteId), eq(resources.http, false))); + .where( + and(eq(resources.siteId, siteId), eq(resources.http, false)) + ); // Get all enabled targets for these resources in a single query const resourceIds = resourcesList.map((r) => r.resourceId); @@ -251,7 +240,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { method: targets.method, port: targets.port, internalPort: targets.internalPort, - enabled: targets.enabled, + enabled: targets.enabled }) .from(targets) .where( From 67ba225003f33f443b67e300ac03fcf763e30865 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 14 Aug 2025 17:06:07 -0700 Subject: [PATCH 122/219] add statistics --- package-lock.json | 28 ++ package.json | 7 +- server/db/sqlite/schema.ts | 1 + server/index.ts | 6 + .../{setup/setHostMeta.ts => lib/hostMeta.ts} | 13 +- server/lib/readConfigFile.ts | 16 +- server/lib/telemetry.ts | 295 ++++++++++++++++++ server/license/license.ts | 2 +- server/logger.ts | 1 + server/setup/migrationsPg.ts | 2 +- server/setup/migrationsSqlite.ts | 2 +- 11 files changed, 362 insertions(+), 11 deletions(-) rename server/{setup/setHostMeta.ts => lib/hostMeta.ts} (57%) create mode 100644 server/lib/telemetry.ts diff --git a/package-lock.json b/package-lock.json index e0d69052..b0dd35e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "npm": "^11.5.2", "oslo": "1.2.1", "pg": "^8.16.2", + "posthog-node": "^5.7.0", "qrcode.react": "4.2.0", "react": "19.1.1", "react-dom": "19.1.1", @@ -2050,6 +2051,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -2983,6 +2985,12 @@ "tslib": "^2.8.1" } }, + "node_modules/@posthog/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.0.0.tgz", + "integrity": "sha512-gquQld+duT9DdzLIFoHZkUMW0DZOTSLCtSjuuC/zKFz65Qecbz9p37DHBJMkw0dCuB8Mgh2GtH8Ag3PznJrP3g==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -6258,6 +6266,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -8798,6 +8807,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9560,6 +9570,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -10405,6 +10416,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10417,6 +10429,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -14183,6 +14196,18 @@ "node": ">=0.10.0" } }, + "node_modules/posthog-node": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.7.0.tgz", + "integrity": "sha512-6J1AIZWtbr2lEbZOO2AzO/h1FPJjUZM4KWcdaL2UQw7FY8J7VNaH3NiaRockASFmglpID7zEY25gV/YwCtuXjg==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -15914,6 +15939,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16552,6 +16578,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16857,6 +16884,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 937f76fa..7b3464a8 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,9 @@ "@radix-ui/react-tooltip": "^1.2.7", "@react-email/components": "0.5.0", "@react-email/render": "^1.2.0", + "@react-email/tailwind": "1.2.2", "@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/server": "^9.0.3", - "@react-email/tailwind": "1.2.2", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", @@ -93,6 +93,7 @@ "npm": "^11.5.2", "oslo": "1.2.1", "pg": "^8.16.2", + "posthog-node": "^5.7.0", "qrcode.react": "4.2.0", "react": "19.1.1", "react-dom": "19.1.1", @@ -109,9 +110,9 @@ "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", "ws": "8.18.3", + "yargs": "18.0.0", "zod": "3.25.76", - "zod-validation-error": "3.5.2", - "yargs": "18.0.0" + "zod-validation-error": "3.5.2" }, "devDependencies": { "@dotenvx/dotenvx": "1.48.4", diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index db1d8090..5bd81d6a 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -690,3 +690,4 @@ export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; +export type HostMeta = InferSelectModel; diff --git a/server/index.ts b/server/index.ts index d3f90281..8724cb41 100644 --- a/server/index.ts +++ b/server/index.ts @@ -8,11 +8,17 @@ import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db"; import { createIntegrationApiServer } from "./integrationApiServer"; import config from "@server/lib/config"; +import { setHostMeta } from "@server/lib/hostMeta"; +import { initTelemetryClient } from "./lib/telemetry.js"; async function startServers() { + await setHostMeta(); + await config.initServer(); await runSetupFunctions(); + initTelemetryClient(); + // Start all servers const apiServer = createApiServer(); const internalServer = createInternalServer(); diff --git a/server/setup/setHostMeta.ts b/server/lib/hostMeta.ts similarity index 57% rename from server/setup/setHostMeta.ts rename to server/lib/hostMeta.ts index 2223d11b..2f2c7ed7 100644 --- a/server/setup/setHostMeta.ts +++ b/server/lib/hostMeta.ts @@ -1,7 +1,9 @@ -import { db } from "@server/db"; +import { db, HostMeta } from "@server/db"; import { hostMeta } from "@server/db"; import { v4 as uuidv4 } from "uuid"; +let gotHostMeta: HostMeta | undefined; + export async function setHostMeta() { const [existing] = await db.select().from(hostMeta).limit(1); @@ -15,3 +17,12 @@ export async function setHostMeta() { .insert(hostMeta) .values({ hostMetaId: id, createdAt: new Date().getTime() }); } + +export async function getHostMeta() { + if (gotHostMeta) { + return gotHostMeta; + } + const [meta] = await db.select().from(hostMeta).limit(1); + gotHostMeta = meta; + return meta; +} diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 1bc119fa..c349e50e 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -3,7 +3,6 @@ import yaml from "js-yaml"; import { configFilePath1, configFilePath2 } from "./consts"; import { z } from "zod"; import stoi from "./stoi"; -import { build } from "@server/build"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -25,7 +24,13 @@ export const configSchema = z .optional() .default("info"), save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false) + log_failed_attempts: z.boolean().optional().default(false), + telmetry: z + .object({ + anonymous_usage: z.boolean().optional().default(true) + }) + .optional() + .default({}) }), domains: z .record( @@ -213,7 +218,10 @@ export const configSchema = z smtp_host: z.string().optional(), smtp_port: portSchema.optional(), smtp_user: z.string().optional(), - smtp_pass: z.string().optional().transform(getEnvOrYaml("EMAIL_SMTP_PASS")), + smtp_pass: z + .string() + .optional() + .transform(getEnvOrYaml("EMAIL_SMTP_PASS")), smtp_secure: z.boolean().optional(), smtp_tls_reject_unauthorized: z.boolean().optional(), no_reply: z.string().email().optional() @@ -229,7 +237,7 @@ export const configSchema = z disable_local_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(), - enable_clients: z.boolean().optional().default(true), + enable_clients: z.boolean().optional().default(true) }) .optional(), dns: z diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts new file mode 100644 index 00000000..8475fb34 --- /dev/null +++ b/server/lib/telemetry.ts @@ -0,0 +1,295 @@ +import { PostHog } from "posthog-node"; +import config from "./config"; +import { getHostMeta } from "./hostMeta"; +import logger from "@server/logger"; +import { apiKeys, db, roles } from "@server/db"; +import { sites, users, orgs, resources, clients, idp } from "@server/db"; +import { eq, count, notInArray } from "drizzle-orm"; +import { APP_VERSION } from "./consts"; +import crypto from "crypto"; +import { UserType } from "@server/types/UserTypes"; + +class TelemetryClient { + private client: PostHog | null = null; + private enabled: boolean; + private intervalId: NodeJS.Timeout | null = null; + + constructor() { + const enabled = config.getRawConfig().app.telmetry.anonymous_usage; + this.enabled = enabled; + const dev = process.env.ENVIRONMENT !== "prod"; + + if (this.enabled && !dev) { + this.client = new PostHog( + "phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX", + { + host: "https://digpangolin.com/relay-O7yI" + } + ); + + process.on("exit", () => { + this.client?.shutdown(); + }); + + this.sendStartupEvents().catch((err) => { + logger.error("Failed to send startup telemetry:", err); + }); + + this.startAnalyticsInterval(); + + logger.info( + "Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry" + ); + } else if (!this.enabled && !dev) { + logger.info( + "Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.digpangolin.com/telemetry" + ); + } + } + + private startAnalyticsInterval() { + this.intervalId = setInterval( + () => { + this.collectAndSendAnalytics().catch((err) => { + logger.error("Failed to collect analytics:", err); + }); + }, + 6 * 60 * 60 * 1000 + ); + + this.collectAndSendAnalytics().catch((err) => { + logger.error("Failed to collect initial analytics:", err); + }); + } + + private anon(value: string): string { + return crypto + .createHash("sha256") + .update(value.toLowerCase()) + .digest("hex"); + } + + private async getSystemStats() { + try { + const [sitesCount] = await db + .select({ count: count() }) + .from(sites); + const [usersCount] = await db + .select({ count: count() }) + .from(users); + const [usersInternalCount] = await db + .select({ count: count() }) + .from(users) + .where(eq(users.type, UserType.Internal)); + const [usersOidcCount] = await db + .select({ count: count() }) + .from(users) + .where(eq(users.type, UserType.OIDC)); + const [orgsCount] = await db.select({ count: count() }).from(orgs); + const [resourcesCount] = await db + .select({ count: count() }) + .from(resources); + const [clientsCount] = await db + .select({ count: count() }) + .from(clients); + const [idpCount] = await db.select({ count: count() }).from(idp); + const [onlineSitesCount] = await db + .select({ count: count() }) + .from(sites) + .where(eq(sites.online, true)); + const [numApiKeys] = await db + .select({ count: count() }) + .from(apiKeys); + const [customRoles] = await db + .select({ count: count() }) + .from(roles) + .where(notInArray(roles.name, ["Admin", "Member"])); + + const adminUsers = await db + .select({ email: users.email }) + .from(users) + .where(eq(users.serverAdmin, true)); + + const resourceDetails = await db + .select({ + name: resources.name, + sso: resources.sso, + protocol: resources.protocol, + http: resources.http + }) + .from(resources); + + const siteDetails = await db + .select({ + siteName: sites.name, + megabytesIn: sites.megabytesIn, + megabytesOut: sites.megabytesOut, + type: sites.type, + online: sites.online + }) + .from(sites); + + const supporterKey = config.getSupporterData(); + + return { + numSites: sitesCount.count, + numUsers: usersCount.count, + numUsersInternal: usersInternalCount.count, + numUsersOidc: usersOidcCount.count, + numOrganizations: orgsCount.count, + numResources: resourcesCount.count, + numClients: clientsCount.count, + numIdentityProviders: idpCount.count, + numSitesOnline: onlineSitesCount.count, + resources: resourceDetails, + adminUsers: adminUsers.map((u) => u.email), + sites: siteDetails, + appVersion: APP_VERSION, + numApiKeys: numApiKeys.count, + numCustomRoles: customRoles.count, + supporterStatus: { + valid: supporterKey?.valid || false, + tier: supporterKey?.tier || "None", + githubUsername: supporterKey?.githubUsername || null + } + }; + } catch (error) { + logger.error("Failed to collect system stats:", error); + throw error; + } + } + + private async sendStartupEvents() { + if (!this.enabled || !this.client) return; + + const hostMeta = await getHostMeta(); + if (!hostMeta) return; + + const stats = await this.getSystemStats(); + + this.client.capture({ + distinctId: hostMeta.hostMetaId, + event: "supporter_status", + properties: { + valid: stats.supporterStatus.valid, + tier: stats.supporterStatus.tier, + github_username: stats.supporterStatus.githubUsername + ? this.anon(stats.supporterStatus.githubUsername) + : "None" + } + }); + + this.client.capture({ + distinctId: hostMeta.hostMetaId, + event: "host_startup", + properties: { + host_id: hostMeta.hostMetaId, + app_version: stats.appVersion, + install_timestamp: hostMeta.createdAt + } + }); + + for (const email of stats.adminUsers) { + // There should only be on admin user, but just in case + if (email) { + this.client.capture({ + distinctId: this.anon(email), + event: "admin_user", + properties: { + host_id: hostMeta.hostMetaId, + app_version: stats.appVersion, + hashed_email: this.anon(email) + } + }); + } + } + } + + private async collectAndSendAnalytics() { + if (!this.enabled || !this.client) return; + + try { + const hostMeta = await getHostMeta(); + if (!hostMeta) { + logger.warn( + "Telemetry: Host meta not found, skipping analytics" + ); + return; + } + + const stats = await this.getSystemStats(); + + this.client.capture({ + distinctId: hostMeta.hostMetaId, + event: "system_analytics", + properties: { + app_version: stats.appVersion, + num_sites: stats.numSites, + num_users: stats.numUsers, + num_users_internal: stats.numUsersInternal, + num_users_oidc: stats.numUsersOidc, + num_organizations: stats.numOrganizations, + num_resources: stats.numResources, + num_clients: stats.numClients, + num_identity_providers: stats.numIdentityProviders, + num_sites_online: stats.numSitesOnline, + resources: stats.resources.map((r) => ({ + name: this.anon(r.name), + sso_enabled: r.sso, + protocol: r.protocol, + http_enabled: r.http + })), + sites: stats.sites.map((s) => ({ + site_name: this.anon(s.siteName), + megabytes_in: s.megabytesIn, + megabytes_out: s.megabytesOut, + type: s.type, + online: s.online + })), + num_api_keys: stats.numApiKeys, + num_custom_roles: stats.numCustomRoles + } + }); + } catch (error) { + logger.error("Failed to send analytics:", error); + } + } + + async sendTelemetry(eventName: string, properties: Record) { + if (!this.enabled || !this.client) return; + + const hostMeta = await getHostMeta(); + if (!hostMeta) { + logger.warn("Telemetry: Host meta not found, skipping telemetry"); + return; + } + + this.client.groupIdentify({ + groupType: "host_id", + groupKey: hostMeta.hostMetaId, + properties + }); + } + + shutdown() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + if (this.enabled && this.client) { + this.client.shutdown(); + } + } +} + +let telemetryClient!: TelemetryClient; + +export function initTelemetryClient() { + if (!telemetryClient) { + telemetryClient = new TelemetryClient(); + } + return telemetryClient; +} + +export default telemetryClient; diff --git a/server/license/license.ts b/server/license/license.ts index 0adc54fd..aeb628df 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -5,7 +5,7 @@ import NodeCache from "node-cache"; import { validateJWT } from "./licenseJwt"; import { count, eq } from "drizzle-orm"; import moment from "moment"; -import { setHostMeta } from "@server/setup/setHostMeta"; +import { setHostMeta } from "@server/lib/hostMeta"; import { encrypt, decrypt } from "@server/lib/crypto"; const keyTypes = ["HOST", "SITES"] as const; diff --git a/server/logger.ts b/server/logger.ts index cd12d735..15dd6e3f 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -3,6 +3,7 @@ import config from "@server/lib/config"; import * as winston from "winston"; import path from "path"; import { APP_PATH } from "./lib/consts"; +import telemetryClient from "./lib/telemetry"; const hformat = winston.format.printf( ({ level, label, message, timestamp, stack, ...metadata }) => { diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 6b3f20b9..fd9a7c21 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -18,7 +18,7 @@ const migrations = [ { version: "1.6.0", run: m1 }, { version: "1.7.0", run: m2 }, { version: "1.8.0", run: m3 }, - { version: "1.9.0", run: m4 } + // { version: "1.9.0", run: m4 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 5b0850c8..5411261f 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -50,7 +50,7 @@ const migrations = [ { version: "1.6.0", run: m21 }, { version: "1.7.0", run: m22 }, { version: "1.8.0", run: m23 }, - { version: "1.9.0", run: m24 }, + // { version: "1.9.0", run: m24 }, // Add new migrations here as they are created ] as const; From 2c96eb78512d925dc993c6cb4fd9d0938cfb831f Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 17:57:50 -0700 Subject: [PATCH 123/219] Adding and removing peers working; better axios errors --- server/hybridServer.ts | 54 +++++++++----------- server/lib/remoteTraefikConfig.ts | 83 +++++++++++++++++-------------- 2 files changed, 71 insertions(+), 66 deletions(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index adf9ce25..c0d342cf 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -33,24 +33,24 @@ export async function createHybridClientServer() { // Register message handlers client.registerHandler("remoteExitNode/peers/add", async (message) => { - const { pubKey, allowedIps } = message.data; + const { publicKey, allowedIps } = message.data; // TODO: we are getting the exit node twice here // NOTE: there should only be one gerbil registered so... const [exitNode] = await db.select().from(exitNodes).limit(1); await addPeer(exitNode.exitNodeId, { - publicKey: pubKey, + publicKey: publicKey, allowedIps: allowedIps || [] }); }); client.registerHandler("remoteExitNode/peers/remove", async (message) => { - const { pubKey } = message.data; + const { publicKey } = message.data; // TODO: we are getting the exit node twice here // NOTE: there should only be one gerbil registered so... const [exitNode] = await db.select().from(exitNodes).limit(1); - await deletePeer(exitNode.exitNodeId, pubKey); + await deletePeer(exitNode.exitNodeId, publicKey); }); // /update-proxy-mapping @@ -65,21 +65,18 @@ export async function createHybridClientServer() { const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data); logger.info(`Successfully updated proxy mapping: ${response.status}`); } catch (error) { - // Extract useful information from axios error without circular references - if (error && typeof error === 'object' && 'response' in error) { - const axiosError = error as any; - logger.error("Failed to update proxy mapping:", { - status: axiosError.response?.status, - statusText: axiosError.response?.statusText, - data: axiosError.response?.data, - message: axiosError.message, - url: axiosError.config?.url + // pull data out of the axios error to log + if (axios.isAxiosError(error)) { + logger.error("Error updating proxy mapping:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method }); } else { - logger.error("Failed to update proxy mapping:", { - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined - }); + logger.error("Error updating proxy mapping:", error); } } }); @@ -96,21 +93,18 @@ export async function createHybridClientServer() { const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data); logger.info(`Successfully updated destinations: ${response.status}`); } catch (error) { - // Extract useful information from axios error without circular references - if (error && typeof error === 'object' && 'response' in error) { - const axiosError = error as any; - logger.error("Failed to update destinations:", { - status: axiosError.response?.status, - statusText: axiosError.response?.statusText, - data: axiosError.response?.data, - message: axiosError.message, - url: axiosError.config?.url + // pull data out of the axios error to log + if (axios.isAxiosError(error)) { + logger.error("Error updating destinations:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method }); } else { - logger.error("Failed to update proxy mapping:", { - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined - }); + logger.error("Error updating destinations:", error); } } }); diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index 36bdad24..88ea011a 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -87,8 +87,7 @@ export class TraefikConfigManager { public async HandleTraefikConfig(): Promise { try { // Get all active domains for this exit node via HTTP call - const getTraefikConfig = - await this.getTraefikConfig(); + const getTraefikConfig = await this.getTraefikConfig(); if (!getTraefikConfig) { logger.error( @@ -138,12 +137,32 @@ export class TraefikConfigManager { try { const [exitNode] = await db.select().from(exitNodes).limit(1); if (exitNode) { + try { + await axios.post( + `${exitNode.reachableAt}/update-local-snis`, + { fullDomains: Array.from(domains) }, + { headers: { "Content-Type": "application/json" } } + ); + } catch (error) { + // pull data out of the axios error to log + if (axios.isAxiosError(error)) { + logger.error("Error updating local SNI:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error( + "Error updating local SNI:", + error + ); + } + } + } else { logger.error("No exit node found"); - await axios.post( - `${exitNode.reachableAt}/update-local-snis`, - { fullDomains: Array.from(domains) }, - { headers: { "Content-Type": "application/json" } } - ); } } catch (err) { logger.error("Failed to post domains to SNI proxy:", err); @@ -199,22 +218,19 @@ export class TraefikConfigManager { ); return { domains, traefikConfig }; - } catch (err) { - // Extract useful information from axios error without circular references - if (err && typeof err === 'object' && 'response' in err) { - const axiosError = err as any; - logger.error("Failed to fetch traefik config:", { - status: axiosError.response?.status, - statusText: axiosError.response?.statusText, - data: axiosError.response?.data, - message: axiosError.message, - url: axiosError.config?.url + } catch (error) { + // pull data out of the axios error to log + if (axios.isAxiosError(error)) { + logger.error("Error fetching traefik config:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method }); } else { - logger.error("Failed to fetch traefik config:", { - message: err instanceof Error ? err.message : String(err), - stack: err instanceof Error ? err.stack : undefined - }); + logger.error("Error fetching traefik config:", error); } return null; } @@ -303,23 +319,18 @@ export class TraefikConfigManager { return response.data.data; } catch (error) { - // Extract useful information from axios error without circular references - if (error && typeof error === 'object' && 'response' in error) { - const axiosError = error as any; - logger.error("Error fetching certificates for domains:", { - status: axiosError.response?.status, - statusText: axiosError.response?.statusText, - data: axiosError.response?.data, - message: axiosError.message, - url: axiosError.config?.url, - domains: domainArray + // pull data out of the axios error to log + if (axios.isAxiosError(error)) { + logger.error("Error getting certificates:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method }); } else { - logger.error("Error fetching certificates for domains:", { - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - domains: domainArray - }); + logger.error("Error getting certificates:", error); } return []; } From 5c04b1e14ab3d33ea18d8974619c3a4cf40cd734 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 14 Aug 2025 18:24:21 -0700 Subject: [PATCH 124/219] add site targets, client resources, and auto login --- config/config.example.yml | 50 +- messages/en-US.json | 108 +- server/auth/actions.ts | 5 + server/db/pg/schema.ts | 30 +- server/db/sqlite/schema.ts | 40 +- server/middlewares/index.ts | 1 + .../middlewares/verifySiteResourceAccess.ts | 62 ++ server/routers/client/targets.ts | 39 + server/routers/external.ts | 84 +- server/routers/integration.ts | 7 - server/routers/newt/handleGetConfigMessage.ts | 93 +- .../routers/newt/handleNewtRegisterMessage.ts | 97 +- server/routers/newt/targets.ts | 33 +- server/routers/resource/createResource.ts | 26 +- server/routers/resource/deleteResource.ts | 76 +- server/routers/resource/getResource.ts | 11 +- .../routers/resource/getResourceAuthInfo.ts | 4 +- server/routers/resource/getUserResources.ts | 88 +- server/routers/resource/index.ts | 3 +- server/routers/resource/listResources.ts | 176 ++-- server/routers/resource/transferResource.ts | 214 ---- server/routers/resource/updateResource.ts | 8 +- server/routers/role/addRoleSite.ts | 24 +- server/routers/role/index.ts | 4 +- server/routers/role/removeRoleSite.ts | 32 +- .../siteResource/createSiteResource.ts | 171 ++++ .../siteResource/deleteSiteResource.ts | 124 +++ .../routers/siteResource/getSiteResource.ts | 83 ++ server/routers/siteResource/index.ts | 6 + .../siteResource/listAllSiteResourcesByOrg.ts | 111 +++ .../routers/siteResource/listSiteResources.ts | 118 +++ .../siteResource/updateSiteResource.ts | 196 ++++ server/routers/target/createTarget.ts | 36 +- server/routers/target/deleteTarget.ts | 64 +- server/routers/target/getTarget.ts | 6 +- server/routers/target/helpers.ts | 56 +- server/routers/target/index.ts | 2 +- server/routers/target/listTargets.ts | 9 +- server/routers/target/updateTarget.ts | 41 +- server/routers/traefik/getTraefikConfig.ts | 171 ++-- server/routers/user/addUserSite.ts | 22 +- server/routers/user/removeUserSite.ts | 32 +- server/routers/ws/messageHandlers.ts | 2 +- server/setup/scriptsPg/1.9.0.ts | 2 +- server/setup/scriptsSqlite/1.9.0.ts | 2 +- .../settings/resources/ResourcesDataTable.tsx | 36 - .../resources/ResourcesSplashCard.tsx | 70 -- .../settings/resources/ResourcesTable.tsx | 662 ++++++++++++- .../[resourceId]/ResourceInfoBox.tsx | 29 +- .../[resourceId]/authentication/page.tsx | 141 ++- .../resources/[resourceId]/general/page.tsx | 238 +---- .../resources/[resourceId]/layout.tsx | 15 - .../resources/[resourceId]/proxy/page.tsx | 923 +++++++++++------ .../settings/resources/create/page.tsx | 925 +++++++++++++++--- src/app/[orgId]/settings/resources/page.tsx | 60 +- .../share-links/CreateShareLinkForm.tsx | 11 +- .../settings/share-links/ShareLinksTable.tsx | 4 +- .../settings/sites/[niceId]/SiteInfoCard.tsx | 4 +- .../settings/sites/[niceId]/general/page.tsx | 128 ++- .../[orgId]/settings/sites/create/page.tsx | 2 +- src/app/auth/initial-setup/page.tsx | 10 +- .../[resourceId]/AutoLoginHandler.tsx | 100 ++ src/app/auth/resource/[resourceId]/page.tsx | 21 +- src/components/ContainersSelector.tsx | 76 +- .../CreateInternalResourceDialog.tsx | 422 ++++++++ src/components/DomainPicker.tsx | 903 ++++++++++------- src/components/EditInternalResourceDialog.tsx | 276 ++++++ src/components/LayoutSidebar.tsx | 2 +- src/components/OrgSelector.tsx | 2 +- src/components/SidebarNav.tsx | 2 +- src/components/ui/alert.tsx | 2 +- src/components/ui/data-table.tsx | 82 +- src/components/ui/input-otp.tsx | 2 +- src/components/ui/input.tsx | 4 +- src/components/ui/select.tsx | 2 +- src/components/ui/tabs.tsx | 2 +- src/contexts/resourceContext.ts | 2 - src/hooks/useDockerSocket.ts | 168 ---- src/lib/docker.ts | 136 +++ src/providers/ResourceProvider.tsx | 5 +- 80 files changed, 5651 insertions(+), 2385 deletions(-) create mode 100644 server/middlewares/verifySiteResourceAccess.ts create mode 100644 server/routers/client/targets.ts delete mode 100644 server/routers/resource/transferResource.ts create mode 100644 server/routers/siteResource/createSiteResource.ts create mode 100644 server/routers/siteResource/deleteSiteResource.ts create mode 100644 server/routers/siteResource/getSiteResource.ts create mode 100644 server/routers/siteResource/index.ts create mode 100644 server/routers/siteResource/listAllSiteResourcesByOrg.ts create mode 100644 server/routers/siteResource/listSiteResources.ts create mode 100644 server/routers/siteResource/updateSiteResource.ts delete mode 100644 src/app/[orgId]/settings/resources/ResourcesDataTable.tsx delete mode 100644 src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx create mode 100644 src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx create mode 100644 src/components/CreateInternalResourceDialog.tsx create mode 100644 src/components/EditInternalResourceDialog.tsx delete mode 100644 src/hooks/useDockerSocket.ts create mode 100644 src/lib/docker.ts diff --git a/config/config.example.yml b/config/config.example.yml index c5f70641..fcb7edde 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -2,47 +2,27 @@ # https://docs.digpangolin.com/self-host/advanced/config-file app: - dashboard_url: "http://localhost:3002" - log_level: "info" - save_logs: false + dashboard_url: http://localhost:3002 + log_level: debug domains: - domain1: - base_domain: "example.com" - cert_resolver: "letsencrypt" + domain1: + base_domain: example.com server: - external_port: 3000 - internal_port: 3001 - next_port: 3002 - internal_hostname: "pangolin" - session_cookie_name: "p_session_token" - resource_access_token_param: "p_token" - secret: "your_secret_key_here" - resource_access_token_headers: - id: "P-Access-Token-Id" - token: "P-Access-Token" - resource_session_request_param: "p_session_request" - -traefik: - http_entrypoint: "web" - https_entrypoint: "websecure" + secret: my_secret_key gerbil: - start_port: 51820 - base_endpoint: "localhost" - block_size: 24 - site_block_size: 30 - subnet_group: 100.89.137.0/20 - use_subdomain: true + base_endpoint: example.com -rate_limits: - global: - window_minutes: 1 - max_requests: 500 +orgs: + block_size: 24 + subnet_group: 100.90.137.0/20 flags: - require_email_verification: false - disable_signup_without_invite: true - disable_user_create_org: true - allow_raw_resources: true + require_email_verification: false + disable_signup_without_invite: true + disable_user_create_org: true + allow_raw_resources: true + enable_integration_api: true + enable_clients: true diff --git a/messages/en-US.json b/messages/en-US.json index 1a3fdfa8..6f80cbe9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -166,7 +166,7 @@ "siteSelect": "Select site", "siteSearch": "Search site", "siteNotFound": "No site found.", - "siteSelectionDescription": "This site will provide connectivity to the resource.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Resource Type", "resourceTypeDescription": "Determine how you want to access your resource", "resourceHTTPSSettings": "HTTPS Settings", @@ -197,6 +197,7 @@ "general": "General", "generalSettings": "General Settings", "proxy": "Proxy", + "internal": "Internal", "rules": "Rules", "resourceSettingDescription": "Configure the settings on your resource", "resourceSetting": "{resourceName} Settings", @@ -490,7 +491,7 @@ "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSubmit": "Save Settings", "targets": "Targets Configuration", - "targetsDescription": "Set up targets to route traffic to your services", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Enable Sticky Sessions", "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "methodSelect": "Select method", @@ -986,7 +987,7 @@ "actionGetSite": "Get Site", "actionListSites": "List Sites", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", @@ -1345,9 +1346,106 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", "siteConfiguration": "Configuration", "siteAcceptClientConnections": "Accept Client Connections", "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to." -} \ No newline at end of file + "siteAddressDescription": "Specify the IP address of the host for clients to connect to.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} diff --git a/server/auth/actions.ts b/server/auth/actions.ts index ee2c5dac..a3ad60ab 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -69,6 +69,11 @@ export enum ActionsEnum { deleteResourceRule = "deleteResourceRule", listResourceRules = "listResourceRules", updateResourceRule = "updateResourceRule", + createSiteResource = "createSiteResource", + deleteSiteResource = "deleteSiteResource", + getSiteResource = "getSiteResource", + listSiteResources = "listSiteResources", + updateSiteResource = "updateSiteResource", createClient = "createClient", deleteClient = "deleteClient", updateClient = "updateClient", diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 2ba10e3e..a2ec521e 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -66,11 +66,6 @@ export const sites = pgTable("sites", { export const resources = pgTable("resources", { resourceId: serial("resourceId").primaryKey(), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), orgId: varchar("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -97,6 +92,9 @@ export const resources = pgTable("resources", { tlsServerName: varchar("tlsServerName"), setHostHeader: varchar("setHostHeader"), enableProxy: boolean("enableProxy").default(true), + skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { + onDelete: "cascade" + }), }); export const targets = pgTable("targets", { @@ -106,6 +104,11 @@ export const targets = pgTable("targets", { onDelete: "cascade" }) .notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), ip: varchar("ip").notNull(), method: varchar("method"), port: integer("port").notNull(), @@ -124,6 +127,22 @@ export const exitNodes = pgTable("exitNodes", { maxConnections: integer("maxConnections") }); +export const siteResources = pgTable("siteResources", { // this is for the clients + siteResourceId: serial("siteResourceId").primaryKey(), + siteId: integer("siteId") + .notNull() + .references(() => sites.siteId, { onDelete: "cascade" }), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + name: varchar("name").notNull(), + protocol: varchar("protocol").notNull(), + proxyPort: integer("proxyPort").notNull(), + destinationPort: integer("destinationPort").notNull(), + destinationIp: varchar("destinationIp").notNull(), + enabled: boolean("enabled").notNull().default(true), +}); + export const users = pgTable("user", { userId: varchar("id").primaryKey(), email: varchar("email"), @@ -647,4 +666,5 @@ export type OlmSession = InferSelectModel; export type UserClient = InferSelectModel; export type RoleClient = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SiteResource = InferSelectModel; export type SetupToken = InferSelectModel; diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 5bd81d6a..3dde2dd7 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -67,16 +67,11 @@ export const sites = sqliteTable("sites", { dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) .notNull() .default(true), - remoteSubnets: text("remoteSubnets"), // comma-separated list of subnets that this site can access + remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access }); export const resources = sqliteTable("resources", { resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -109,6 +104,9 @@ export const resources = sqliteTable("resources", { tlsServerName: text("tlsServerName"), setHostHeader: text("setHostHeader"), enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), + skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { + onDelete: "cascade" + }), }); export const targets = sqliteTable("targets", { @@ -118,6 +116,11 @@ export const targets = sqliteTable("targets", { onDelete: "cascade" }) .notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), ip: text("ip").notNull(), method: text("method"), port: integer("port").notNull(), @@ -136,6 +139,22 @@ export const exitNodes = sqliteTable("exitNodes", { maxConnections: integer("maxConnections") }); +export const siteResources = sqliteTable("siteResources", { // this is for the clients + siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }), + siteId: integer("siteId") + .notNull() + .references(() => sites.siteId, { onDelete: "cascade" }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + name: text("name").notNull(), + protocol: text("protocol").notNull(), + proxyPort: integer("proxyPort").notNull(), + destinationPort: integer("destinationPort").notNull(), + destinationIp: text("destinationIp").notNull(), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), +}); + export const users = sqliteTable("user", { userId: text("id").primaryKey(), email: text("email"), @@ -166,9 +185,11 @@ export const users = sqliteTable("user", { export const securityKeys = sqliteTable("webauthnCredentials", { credentialId: text("credentialId").primaryKey(), - userId: text("userId").notNull().references(() => users.userId, { - onDelete: "cascade" - }), + userId: text("userId") + .notNull() + .references(() => users.userId, { + onDelete: "cascade" + }), publicKey: text("publicKey").notNull(), signCount: integer("signCount").notNull(), transports: text("transports"), @@ -688,6 +709,7 @@ export type Idp = InferSelectModel; export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; +export type SiteResource = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index b1180995..28a73afd 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -27,3 +27,4 @@ export * from "./verifyApiKeyAccess"; export * from "./verifyDomainAccess"; export * from "./verifyClientsEnabled"; export * from "./verifyUserIsOrgOwner"; +export * from "./verifySiteResourceAccess"; diff --git a/server/middlewares/verifySiteResourceAccess.ts b/server/middlewares/verifySiteResourceAccess.ts new file mode 100644 index 00000000..e7fefd24 --- /dev/null +++ b/server/middlewares/verifySiteResourceAccess.ts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { siteResources } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import logger from "@server/logger"; + +export async function verifySiteResourceAccess( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const siteResourceId = parseInt(req.params.siteResourceId); + const siteId = parseInt(req.params.siteId); + const orgId = req.params.orgId; + + if (!siteResourceId || !siteId || !orgId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Missing required parameters" + ) + ); + } + + // Check if the site resource exists and belongs to the specified site and org + const [siteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + + if (!siteResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site resource not found" + ) + ); + } + + // Attach the siteResource to the request for use in the next middleware/route + // @ts-ignore - Extending Request type + req.siteResource = siteResource; + + next(); + } catch (error) { + logger.error("Error verifying site resource access:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site resource access" + ) + ); + } +} diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts new file mode 100644 index 00000000..8d13d8cf --- /dev/null +++ b/server/routers/client/targets.ts @@ -0,0 +1,39 @@ +import { sendToClient } from "../ws"; + +export async function addTargets( + newtId: string, + destinationIp: string, + destinationPort: number, + protocol: string, + port: number | null = null +) { + const target = `${port ? port + ":" : ""}${ + destinationIp + }:${destinationPort}`; + + await sendToClient(newtId, { + type: `newt/wg/${protocol}/add`, + data: { + targets: [target] // We can only use one target for WireGuard right now + } + }); +} + +export async function removeTargets( + newtId: string, + destinationIp: string, + destinationPort: number, + protocol: string, + port: number | null = null +) { + const target = `${port ? port + ":" : ""}${ + destinationIp + }:${destinationPort}`; + + await sendToClient(newtId, { + type: `newt/wg/${protocol}/remove`, + data: { + targets: [target] // We can only use one target for WireGuard right now + } + }); +} diff --git a/server/routers/external.ts b/server/routers/external.ts index f9ff7377..65dc6108 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -9,6 +9,7 @@ import * as user from "./user"; import * as auth from "./auth"; import * as role from "./role"; import * as client from "./client"; +import * as siteResource from "./siteResource"; import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; @@ -34,7 +35,8 @@ import { verifyDomainAccess, verifyClientsEnabled, verifyUserHasAction, - verifyUserIsOrgOwner + verifyUserIsOrgOwner, + verifySiteResourceAccess } from "@server/middlewares"; import { createStore } from "@server/lib/rateLimitStore"; import { ActionsEnum } from "@server/auth/actions"; @@ -213,9 +215,60 @@ authenticated.get( site.listContainers ); +// Site Resource endpoints authenticated.put( "/org/:orgId/site/:siteId/resource", verifyOrgAccess, + verifySiteAccess, + verifyUserHasAction(ActionsEnum.createSiteResource), + siteResource.createSiteResource +); + +authenticated.get( + "/org/:orgId/site/:siteId/resources", + verifyOrgAccess, + verifySiteAccess, + verifyUserHasAction(ActionsEnum.listSiteResources), + siteResource.listSiteResources +); + +authenticated.get( + "/org/:orgId/site-resources", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listSiteResources), + siteResource.listAllSiteResourcesByOrg +); + +authenticated.get( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyOrgAccess, + verifySiteAccess, + verifySiteResourceAccess, + verifyUserHasAction(ActionsEnum.getSiteResource), + siteResource.getSiteResource +); + +authenticated.post( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyOrgAccess, + verifySiteAccess, + verifySiteResourceAccess, + verifyUserHasAction(ActionsEnum.updateSiteResource), + siteResource.updateSiteResource +); + +authenticated.delete( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyOrgAccess, + verifySiteAccess, + verifySiteResourceAccess, + verifyUserHasAction(ActionsEnum.deleteSiteResource), + siteResource.deleteSiteResource +); + +authenticated.put( + "/org/:orgId/resource", + verifyOrgAccess, verifyUserHasAction(ActionsEnum.createResource), resource.createResource ); @@ -397,28 +450,6 @@ authenticated.post( user.addUserRole ); -// authenticated.put( -// "/role/:roleId/site", -// verifyRoleAccess, -// verifyUserInRole, -// verifyUserHasAction(ActionsEnum.addRoleSite), -// role.addRoleSite -// ); -// authenticated.delete( -// "/role/:roleId/site", -// verifyRoleAccess, -// verifyUserInRole, -// verifyUserHasAction(ActionsEnum.removeRoleSite), -// role.removeRoleSite -// ); -// authenticated.get( -// "/role/:roleId/sites", -// verifyRoleAccess, -// verifyUserInRole, -// verifyUserHasAction(ActionsEnum.listRoleSites), -// role.listRoleSites -// ); - authenticated.post( "/resource/:resourceId/roles", verifyResourceAccess, @@ -463,13 +494,6 @@ authenticated.get( resource.getResourceWhitelist ); -authenticated.post( - `/resource/:resourceId/transfer`, - verifyResourceAccess, - verifyUserHasAction(ActionsEnum.updateResource), - resource.transferResource -); - authenticated.post( `/resource/:resourceId/access-token`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 39939e1c..ee707333 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -341,13 +341,6 @@ authenticated.get( resource.getResourceWhitelist ); -authenticated.post( - `/resource/:resourceId/transfer`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateResource), - resource.transferResource -); - authenticated.post( `/resource/:resourceId/access-token`, verifyApiKeyResourceAccess, diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index b2594a71..7d6b3567 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -220,78 +220,37 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { // Filter out any null values from peers that didn't have an olm const validPeers = peers.filter((peer) => peer !== null); - // Improved version - const allResources = await db.transaction(async (tx) => { - // First get all resources for the site - const resourcesList = await tx - .select({ - resourceId: resources.resourceId, - subdomain: resources.subdomain, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - blockAccess: resources.blockAccess, - sso: resources.sso, - emailWhitelistEnabled: resources.emailWhitelistEnabled, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol - }) - .from(resources) - .where(and(eq(resources.siteId, siteId), eq(resources.http, false))); + // Get all enabled targets with their resource protocol information + const allTargets = await db + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled, + protocol: resources.protocol + }) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .where(and(eq(targets.siteId, siteId), eq(targets.enabled, true))); - // Get all enabled targets for these resources in a single query - const resourceIds = resourcesList.map((r) => r.resourceId); - const allTargets = - resourceIds.length > 0 - ? await tx - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled, - }) - .from(targets) - .where( - and( - inArray(targets.resourceId, resourceIds), - eq(targets.enabled, true) - ) - ) - : []; + const { tcpTargets, udpTargets } = allTargets.reduce( + (acc, target) => { + // Filter out invalid targets + if (!target.internalPort || !target.ip || !target.port) { + return acc; + } - // Combine the data in JS instead of using SQL for the JSON - return resourcesList.map((resource) => ({ - ...resource, - targets: allTargets.filter( - (target) => target.resourceId === resource.resourceId - ) - })); - }); - - const { tcpTargets, udpTargets } = allResources.reduce( - (acc, resource) => { - // Skip resources with no targets - if (!resource.targets?.length) return acc; - - // Format valid targets into strings - const formattedTargets = resource.targets - .filter( - (target: Target) => - resource.proxyPort && target?.ip && target?.port - ) - .map( - (target: Target) => - `${resource.proxyPort}:${target.ip}:${target.port}` - ); + // Format target into string + const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`; // Add to the appropriate protocol array - if (resource.protocol === "tcp") { - acc.tcpTargets.push(...formattedTargets); + if (target.protocol === "tcp") { + acc.tcpTargets.push(formattedTarget); } else { - acc.udpTargets.push(...formattedTargets); + acc.udpTargets.push(formattedTarget); } return acc; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 71a6fd5c..0255e97c 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -105,7 +105,9 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { .limit(1); const blockSize = config.getRawConfig().gerbil.site_block_size; - const subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null); + const subnets = sitesQuery + .map((site) => site.subnet) + .filter((subnet) => subnet !== null); subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`)); const newSubnet = findNextAvailableCidr( subnets, @@ -160,78 +162,37 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { allowedIps: [siteSubnet] }); - // Improved version - const allResources = await db.transaction(async (tx) => { - // First get all resources for the site - const resourcesList = await tx - .select({ - resourceId: resources.resourceId, - subdomain: resources.subdomain, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - blockAccess: resources.blockAccess, - sso: resources.sso, - emailWhitelistEnabled: resources.emailWhitelistEnabled, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol - }) - .from(resources) - .where(eq(resources.siteId, siteId)); + // Get all enabled targets with their resource protocol information + const allTargets = await db + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled, + protocol: resources.protocol + }) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .where(and(eq(targets.siteId, siteId), eq(targets.enabled, true))); - // Get all enabled targets for these resources in a single query - const resourceIds = resourcesList.map((r) => r.resourceId); - const allTargets = - resourceIds.length > 0 - ? await tx - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled - }) - .from(targets) - .where( - and( - inArray(targets.resourceId, resourceIds), - eq(targets.enabled, true) - ) - ) - : []; + const { tcpTargets, udpTargets } = allTargets.reduce( + (acc, target) => { + // Filter out invalid targets + if (!target.internalPort || !target.ip || !target.port) { + return acc; + } - // Combine the data in JS instead of using SQL for the JSON - return resourcesList.map((resource) => ({ - ...resource, - targets: allTargets.filter( - (target) => target.resourceId === resource.resourceId - ) - })); - }); - - const { tcpTargets, udpTargets } = allResources.reduce( - (acc, resource) => { - // Skip resources with no targets - if (!resource.targets?.length) return acc; - - // Format valid targets into strings - const formattedTargets = resource.targets - .filter( - (target: Target) => - target?.internalPort && target?.ip && target?.port - ) - .map( - (target: Target) => - `${target.internalPort}:${target.ip}:${target.port}` - ); + // Format target into string + const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`; // Add to the appropriate protocol array - if (resource.protocol === "tcp") { - acc.tcpTargets.push(...formattedTargets); + if (target.protocol === "tcp") { + acc.tcpTargets.push(formattedTarget); } else { - acc.udpTargets.push(...formattedTargets); + acc.udpTargets.push(formattedTarget); } return acc; diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 642fc2df..91a0ac3f 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -1,7 +1,8 @@ import { Target } from "@server/db"; import { sendToClient } from "../ws"; +import logger from "@server/logger"; -export function addTargets( +export async function addTargets( newtId: string, targets: Target[], protocol: string, @@ -20,22 +21,9 @@ export function addTargets( targets: payloadTargets } }); - - const payloadTargetsResources = targets.map((target) => { - return `${port ? port + ":" : ""}${ - target.ip - }:${target.port}`; - }); - - sendToClient(newtId, { - type: `newt/wg/${protocol}/add`, - data: { - targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now - } - }); } -export function removeTargets( +export async function removeTargets( newtId: string, targets: Target[], protocol: string, @@ -48,23 +36,10 @@ export function removeTargets( }:${target.port}`; }); - sendToClient(newtId, { + await sendToClient(newtId, { type: `newt/${protocol}/remove`, data: { targets: payloadTargets } }); - - const payloadTargetsResources = targets.map((target) => { - return `${port ? port + ":" : ""}${ - target.ip - }:${target.port}`; - }); - - sendToClient(newtId, { - type: `newt/wg/${protocol}/remove`, - data: { - targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now - } - }); } diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 8c80c90c..e3e431ec 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -15,7 +15,6 @@ import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { eq, and } from "drizzle-orm"; -import stoi from "@server/lib/stoi"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { subdomainSchema } from "@server/lib/schemas"; @@ -25,7 +24,6 @@ import { build } from "@server/build"; const createResourceParamsSchema = z .object({ - siteId: z.string().transform(stoi).pipe(z.number().int().positive()), orgId: z.string() }) .strict(); @@ -34,7 +32,6 @@ const createHttpResourceSchema = z .object({ name: z.string().min(1).max(255), subdomain: z.string().nullable().optional(), - siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), domainId: z.string() @@ -53,11 +50,10 @@ const createHttpResourceSchema = z const createRawResourceSchema = z .object({ name: z.string().min(1).max(255), - siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), proxyPort: z.number().int().min(1).max(65535), - enableProxy: z.boolean().default(true) + // enableProxy: z.boolean().default(true) // always true now }) .strict() .refine( @@ -78,7 +74,7 @@ export type CreateResourceResponse = Resource; registry.registerPath({ method: "put", - path: "/org/{orgId}/site/{siteId}/resource", + path: "/org/{orgId}/resource", description: "Create a resource.", tags: [OpenAPITags.Org, OpenAPITags.Resource], request: { @@ -111,7 +107,7 @@ export async function createResource( ); } - const { siteId, orgId } = parsedParams.data; + const { orgId } = parsedParams.data; if (req.user && !req.userOrgRoleId) { return next( @@ -146,7 +142,7 @@ export async function createResource( if (http) { return await createHttpResource( { req, res, next }, - { siteId, orgId } + { orgId } ); } else { if ( @@ -162,7 +158,7 @@ export async function createResource( } return await createRawResource( { req, res, next }, - { siteId, orgId } + { orgId } ); } } catch (error) { @@ -180,12 +176,11 @@ async function createHttpResource( next: NextFunction; }, meta: { - siteId: number; orgId: string; } ) { const { req, res, next } = route; - const { siteId, orgId } = meta; + const { orgId } = meta; const parsedBody = createHttpResourceSchema.safeParse(req.body); if (!parsedBody.success) { @@ -292,7 +287,6 @@ async function createHttpResource( const newResource = await trx .insert(resources) .values({ - siteId, fullDomain, domainId, orgId, @@ -357,12 +351,11 @@ async function createRawResource( next: NextFunction; }, meta: { - siteId: number; orgId: string; } ) { const { req, res, next } = route; - const { siteId, orgId } = meta; + const { orgId } = meta; const parsedBody = createRawResourceSchema.safeParse(req.body); if (!parsedBody.success) { @@ -374,7 +367,7 @@ async function createRawResource( ); } - const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data; + const { name, http, protocol, proxyPort } = parsedBody.data; // if http is false check to see if there is already a resource with the same port and protocol const existingResource = await db @@ -402,13 +395,12 @@ async function createRawResource( const newResource = await trx .insert(resources) .values({ - siteId, orgId, name, http, protocol, proxyPort, - enableProxy + // enableProxy }) .returning(); diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index 99adc5f7..3b0e9df4 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -71,44 +71,44 @@ export async function deleteResource( ); } - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, deletedResource.siteId!)) - .limit(1); - - if (!site) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${deletedResource.siteId} not found` - ) - ); - } - - if (site.pubKey) { - if (site.type == "wireguard") { - await addPeer(site.exitNodeId!, { - publicKey: site.pubKey, - allowedIps: await getAllowedIps(site.siteId) - }); - } else if (site.type == "newt") { - // get the newt on the site by querying the newt table for siteId - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - removeTargets( - newt.newtId, - targetsToBeRemoved, - deletedResource.protocol, - deletedResource.proxyPort - ); - } - } - + // const [site] = await db + // .select() + // .from(sites) + // .where(eq(sites.siteId, deletedResource.siteId!)) + // .limit(1); + // + // if (!site) { + // return next( + // createHttpError( + // HttpCode.NOT_FOUND, + // `Site with ID ${deletedResource.siteId} not found` + // ) + // ); + // } + // + // if (site.pubKey) { + // if (site.type == "wireguard") { + // await addPeer(site.exitNodeId!, { + // publicKey: site.pubKey, + // allowedIps: await getAllowedIps(site.siteId) + // }); + // } else if (site.type == "newt") { + // // get the newt on the site by querying the newt table for siteId + // const [newt] = await db + // .select() + // .from(newts) + // .where(eq(newts.siteId, site.siteId)) + // .limit(1); + // + // removeTargets( + // newt.newtId, + // targetsToBeRemoved, + // deletedResource.protocol, + // deletedResource.proxyPort + // ); + // } + // } + // return response(res, { data: null, success: true, diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 0cffb1cf..a2c1c0d1 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -19,9 +19,7 @@ const getResourceSchema = z }) .strict(); -export type GetResourceResponse = Resource & { - siteName: string; -}; +export type GetResourceResponse = Resource; registry.registerPath({ method: "get", @@ -56,11 +54,9 @@ export async function getResource( .select() .from(resources) .where(eq(resources.resourceId, resourceId)) - .leftJoin(sites, eq(sites.siteId, resources.siteId)) .limit(1); - const resource = resp.resources; - const site = resp.sites; + const resource = resp; if (!resource) { return next( @@ -73,8 +69,7 @@ export async function getResource( return response(res, { data: { - ...resource, - siteName: site?.name + ...resource }, success: true, error: false, diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 64fade89..191221f1 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -31,6 +31,7 @@ export type GetResourceAuthInfoResponse = { blockAccess: boolean; url: string; whitelist: boolean; + skipToIdpId: number | null; }; export async function getResourceAuthInfo( @@ -86,7 +87,8 @@ export async function getResourceAuthInfo( sso: resource.sso, blockAccess: resource.blockAccess, url, - whitelist: resource.emailWhitelistEnabled + whitelist: resource.emailWhitelistEnabled, + skipToIdpId: resource.skipToIdpId }, success: true, error: false, diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 681ec4d0..3d28da6f 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -1,16 +1,14 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; import { and, eq, or, inArray } from "drizzle-orm"; -import { - resources, - userResources, - roleResources, - userOrgs, - roles, +import { + resources, + userResources, + roleResources, + userOrgs, resourcePassword, resourcePincode, - resourceWhitelist, - sites + resourceWhitelist } from "@server/db"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -37,12 +35,7 @@ export async function getUserResources( roleId: userOrgs.roleId }) .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, orgId) - ) - ) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) .limit(1); if (userOrgResult.length === 0) { @@ -71,8 +64,8 @@ export async function getUserResources( // Combine all accessible resource IDs const accessibleResourceIds = [ - ...directResources.map(r => r.resourceId), - ...roleResourceResults.map(r => r.resourceId) + ...directResources.map((r) => r.resourceId), + ...roleResourceResults.map((r) => r.resourceId) ]; if (accessibleResourceIds.length === 0) { @@ -95,11 +88,9 @@ export async function getUserResources( enabled: resources.enabled, sso: resources.sso, protocol: resources.protocol, - emailWhitelistEnabled: resources.emailWhitelistEnabled, - siteName: sites.name + emailWhitelistEnabled: resources.emailWhitelistEnabled }) .from(resources) - .leftJoin(sites, eq(sites.siteId, resources.siteId)) .where( and( inArray(resources.resourceId, accessibleResourceIds), @@ -111,28 +102,61 @@ export async function getUserResources( // Check for password, pincode, and whitelist protection for each resource const resourcesWithAuth = await Promise.all( resourcesData.map(async (resource) => { - const [passwordCheck, pincodeCheck, whitelistCheck] = await Promise.all([ - db.select().from(resourcePassword).where(eq(resourcePassword.resourceId, resource.resourceId)).limit(1), - db.select().from(resourcePincode).where(eq(resourcePincode.resourceId, resource.resourceId)).limit(1), - db.select().from(resourceWhitelist).where(eq(resourceWhitelist.resourceId, resource.resourceId)).limit(1) - ]); + const [passwordCheck, pincodeCheck, whitelistCheck] = + await Promise.all([ + db + .select() + .from(resourcePassword) + .where( + eq( + resourcePassword.resourceId, + resource.resourceId + ) + ) + .limit(1), + db + .select() + .from(resourcePincode) + .where( + eq( + resourcePincode.resourceId, + resource.resourceId + ) + ) + .limit(1), + db + .select() + .from(resourceWhitelist) + .where( + eq( + resourceWhitelist.resourceId, + resource.resourceId + ) + ) + .limit(1) + ]); const hasPassword = passwordCheck.length > 0; const hasPincode = pincodeCheck.length > 0; - const hasWhitelist = whitelistCheck.length > 0 || resource.emailWhitelistEnabled; + const hasWhitelist = + whitelistCheck.length > 0 || resource.emailWhitelistEnabled; return { resourceId: resource.resourceId, name: resource.name, domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, enabled: resource.enabled, - protected: !!(resource.sso || hasPassword || hasPincode || hasWhitelist), + protected: !!( + resource.sso || + hasPassword || + hasPincode || + hasWhitelist + ), protocol: resource.protocol, sso: resource.sso, password: hasPassword, pincode: hasPincode, - whitelist: hasWhitelist, - siteName: resource.siteName + whitelist: hasWhitelist }; }) ); @@ -144,11 +168,13 @@ export async function getUserResources( message: "User resources retrieved successfully", status: HttpCode.OK }); - } catch (error) { console.error("Error fetching user resources:", error); return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Internal server error") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Internal server error" + ) ); } } @@ -165,4 +191,4 @@ export type GetUserResourcesResponse = { protocol: string; }>; }; -}; \ No newline at end of file +}; diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index f97fcdf4..1a2e5c2d 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -16,10 +16,9 @@ export * from "./setResourceWhitelist"; export * from "./getResourceWhitelist"; export * from "./authWithWhitelist"; export * from "./authWithAccessToken"; -export * from "./transferResource"; export * from "./getExchangeToken"; export * from "./createResourceRule"; export * from "./deleteResourceRule"; export * from "./listResourceRules"; export * from "./updateResourceRule"; -export * from "./getUserResources"; \ No newline at end of file +export * from "./getUserResources"; diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 6df56001..43757b27 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import { db } from "@server/db"; import { resources, - sites, userResources, roleResources, resourcePassword, @@ -20,17 +19,9 @@ import { OpenAPITags, registry } from "@server/openApi"; const listResourcesParamsSchema = z .object({ - siteId: z - .string() - .optional() - .transform(stoi) - .pipe(z.number().int().positive().optional()), - orgId: z.string().optional() + orgId: z.string() }) - .strict() - .refine((data) => !!data.siteId !== !!data.orgId, { - message: "Either siteId or orgId must be provided, but not both" - }); + .strict(); const listResourcesSchema = z.object({ limit: z @@ -48,82 +39,38 @@ const listResourcesSchema = z.object({ .pipe(z.number().int().nonnegative()) }); -function queryResources( - accessibleResourceIds: number[], - siteId?: number, - orgId?: string -) { - if (siteId) { - return db - .select({ - resourceId: resources.resourceId, - name: resources.name, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - siteName: sites.name, - siteId: sites.niceId, - passwordId: resourcePassword.passwordId, - pincodeId: resourcePincode.pincodeId, - sso: resources.sso, - whitelist: resources.emailWhitelistEnabled, - http: resources.http, - protocol: resources.protocol, - proxyPort: resources.proxyPort, - enabled: resources.enabled, - domainId: resources.domainId - }) - .from(resources) - .leftJoin(sites, eq(resources.siteId, sites.siteId)) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) +function queryResources(accessibleResourceIds: number[], orgId: string) { + return db + .select({ + resourceId: resources.resourceId, + name: resources.name, + ssl: resources.ssl, + fullDomain: resources.fullDomain, + passwordId: resourcePassword.passwordId, + sso: resources.sso, + pincodeId: resourcePincode.pincodeId, + whitelist: resources.emailWhitelistEnabled, + http: resources.http, + protocol: resources.protocol, + proxyPort: resources.proxyPort, + enabled: resources.enabled, + domainId: resources.domainId + }) + .from(resources) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .where( + and( + inArray(resources.resourceId, accessibleResourceIds), + eq(resources.orgId, orgId) ) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .where( - and( - inArray(resources.resourceId, accessibleResourceIds), - eq(resources.siteId, siteId) - ) - ); - } else if (orgId) { - return db - .select({ - resourceId: resources.resourceId, - name: resources.name, - ssl: resources.ssl, - fullDomain: resources.fullDomain, - siteName: sites.name, - siteId: sites.niceId, - passwordId: resourcePassword.passwordId, - sso: resources.sso, - pincodeId: resourcePincode.pincodeId, - whitelist: resources.emailWhitelistEnabled, - http: resources.http, - protocol: resources.protocol, - proxyPort: resources.proxyPort, - enabled: resources.enabled, - domainId: resources.domainId - }) - .from(resources) - .leftJoin(sites, eq(resources.siteId, sites.siteId)) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .where( - and( - inArray(resources.resourceId, accessibleResourceIds), - eq(resources.orgId, orgId) - ) - ); - } + ); } export type ListResourcesResponse = { @@ -131,20 +78,6 @@ export type ListResourcesResponse = { pagination: { total: number; limit: number; offset: number }; }; -registry.registerPath({ - method: "get", - path: "/site/{siteId}/resources", - description: "List resources for a site.", - tags: [OpenAPITags.Site, OpenAPITags.Resource], - request: { - params: z.object({ - siteId: z.number() - }), - query: listResourcesSchema - }, - responses: {} -}); - registry.registerPath({ method: "get", path: "/org/{orgId}/resources", @@ -185,9 +118,11 @@ export async function listResources( ) ); } - const { siteId } = parsedParams.data; - const orgId = parsedParams.data.orgId || req.userOrg?.orgId || req.apiKeyOrg?.orgId; + const orgId = + parsedParams.data.orgId || + req.userOrg?.orgId || + req.apiKeyOrg?.orgId; if (!orgId) { return next( @@ -207,24 +142,27 @@ export async function listResources( let accessibleResources; if (req.user) { accessibleResources = await db - .select({ - resourceId: sql`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})` - }) - .from(userResources) - .fullJoin( - roleResources, - eq(userResources.resourceId, roleResources.resourceId) - ) - .where( - or( - eq(userResources.userId, req.user!.userId), - eq(roleResources.roleId, req.userOrgRoleId!) + .select({ + resourceId: sql`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})` + }) + .from(userResources) + .fullJoin( + roleResources, + eq(userResources.resourceId, roleResources.resourceId) ) - ); + .where( + or( + eq(userResources.userId, req.user!.userId), + eq(roleResources.roleId, req.userOrgRoleId!) + ) + ); } else { - accessibleResources = await db.select({ - resourceId: resources.resourceId - }).from(resources).where(eq(resources.orgId, orgId)); + accessibleResources = await db + .select({ + resourceId: resources.resourceId + }) + .from(resources) + .where(eq(resources.orgId, orgId)); } const accessibleResourceIds = accessibleResources.map( @@ -236,7 +174,7 @@ export async function listResources( .from(resources) .where(inArray(resources.resourceId, accessibleResourceIds)); - const baseQuery = queryResources(accessibleResourceIds, siteId, orgId); + const baseQuery = queryResources(accessibleResourceIds, orgId); const resourcesList = await baseQuery!.limit(limit).offset(offset); const totalCountResult = await countQuery; diff --git a/server/routers/resource/transferResource.ts b/server/routers/resource/transferResource.ts deleted file mode 100644 index a99405df..00000000 --- a/server/routers/resource/transferResource.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { newts, resources, sites, targets } from "@server/db"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { addPeer } from "../gerbil/peers"; -import { addTargets, removeTargets } from "../newt/targets"; -import { getAllowedIps } from "../target/helpers"; -import { OpenAPITags, registry } from "@server/openApi"; - -const transferResourceParamsSchema = z - .object({ - resourceId: z - .string() - .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); - -const transferResourceBodySchema = z - .object({ - siteId: z.number().int().positive() - }) - .strict(); - -registry.registerPath({ - method: "post", - path: "/resource/{resourceId}/transfer", - description: - "Transfer a resource to a different site. This will also transfer the targets associated with the resource.", - tags: [OpenAPITags.Resource], - request: { - params: transferResourceParamsSchema, - body: { - content: { - "application/json": { - schema: transferResourceBodySchema - } - } - } - }, - responses: {} -}); - -export async function transferResource( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = transferResourceParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = transferResourceBodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { resourceId } = parsedParams.data; - const { siteId } = parsedBody.data; - - const [oldResource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!oldResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (oldResource.siteId === siteId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Resource is already assigned to this site` - ) - ); - } - - const [newSite] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (!newSite) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` - ) - ); - } - - const [oldSite] = await db - .select() - .from(sites) - .where(eq(sites.siteId, oldResource.siteId)) - .limit(1); - - if (!oldSite) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${oldResource.siteId} not found` - ) - ); - } - - const [updatedResource] = await db - .update(resources) - .set({ siteId }) - .where(eq(resources.resourceId, resourceId)) - .returning(); - - if (!updatedResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - const resourceTargets = await db - .select() - .from(targets) - .where(eq(targets.resourceId, resourceId)); - - if (resourceTargets.length > 0) { - ////// REMOVE THE TARGETS FROM THE OLD SITE ////// - if (oldSite.pubKey) { - if (oldSite.type == "wireguard") { - await addPeer(oldSite.exitNodeId!, { - publicKey: oldSite.pubKey, - allowedIps: await getAllowedIps(oldSite.siteId) - }); - } else if (oldSite.type == "newt") { - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, oldSite.siteId)) - .limit(1); - - removeTargets( - newt.newtId, - resourceTargets, - updatedResource.protocol, - updatedResource.proxyPort - ); - } - } - - ////// ADD THE TARGETS TO THE NEW SITE ////// - if (newSite.pubKey) { - if (newSite.type == "wireguard") { - await addPeer(newSite.exitNodeId!, { - publicKey: newSite.pubKey, - allowedIps: await getAllowedIps(newSite.siteId) - }); - } else if (newSite.type == "newt") { - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, newSite.siteId)) - .limit(1); - - addTargets( - newt.newtId, - resourceTargets, - updatedResource.protocol, - updatedResource.proxyPort - ); - } - } - } - - return response(res, { - data: updatedResource, - success: true, - error: false, - message: "Resource transferred successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 5cf68c2b..30acc0c1 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -20,7 +20,6 @@ import { tlsNameSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas"; import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; -import { build } from "@server/build"; const updateResourceParamsSchema = z .object({ @@ -44,7 +43,8 @@ const updateHttpResourceBodySchema = z enabled: z.boolean().optional(), stickySession: z.boolean().optional(), tlsServerName: z.string().nullable().optional(), - setHostHeader: z.string().nullable().optional() + setHostHeader: z.string().nullable().optional(), + skipToIdpId: z.number().int().positive().nullable().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -91,8 +91,8 @@ const updateRawResourceBodySchema = z name: z.string().min(1).max(255).optional(), proxyPort: z.number().int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), - enabled: z.boolean().optional(), - enableProxy: z.boolean().optional() + enabled: z.boolean().optional() + // enableProxy: z.boolean().optional() // always true now }) .strict() .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/role/addRoleSite.ts b/server/routers/role/addRoleSite.ts index 58da9879..d268eed4 100644 --- a/server/routers/role/addRoleSite.ts +++ b/server/routers/role/addRoleSite.ts @@ -60,18 +60,18 @@ export async function addRoleSite( }) .returning(); - const siteResources = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx.insert(roleResources).values({ - roleId, - resourceId: resource.resourceId - }); - } - + // const siteResources = await db + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx.insert(roleResources).values({ + // roleId, + // resourceId: resource.resourceId + // }); + // } + // return response(res, { data: newRoleSite[0], success: true, diff --git a/server/routers/role/index.ts b/server/routers/role/index.ts index 0194c1f0..bbbe4ba8 100644 --- a/server/routers/role/index.ts +++ b/server/routers/role/index.ts @@ -1,6 +1,5 @@ export * from "./addRoleAction"; export * from "../resource/setResourceRoles"; -export * from "./addRoleSite"; export * from "./createRole"; export * from "./deleteRole"; export * from "./getRole"; @@ -11,5 +10,4 @@ export * from "./listRoles"; export * from "./listRoleSites"; export * from "./removeRoleAction"; export * from "./removeRoleResource"; -export * from "./removeRoleSite"; -export * from "./updateRole"; \ No newline at end of file +export * from "./updateRole"; diff --git a/server/routers/role/removeRoleSite.ts b/server/routers/role/removeRoleSite.ts index c88e4711..2670272d 100644 --- a/server/routers/role/removeRoleSite.ts +++ b/server/routers/role/removeRoleSite.ts @@ -71,22 +71,22 @@ export async function removeRoleSite( ); } - const siteResources = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx - .delete(roleResources) - .where( - and( - eq(roleResources.roleId, roleId), - eq(roleResources.resourceId, resource.resourceId) - ) - ) - .returning(); - } + // const siteResources = await db + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx + // .delete(roleResources) + // .where( + // and( + // eq(roleResources.roleId, roleId), + // eq(roleResources.resourceId, resource.resourceId) + // ) + // ) + // .returning(); + // } }); return response(res, { diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts new file mode 100644 index 00000000..4d80c7a0 --- /dev/null +++ b/server/routers/siteResource/createSiteResource.ts @@ -0,0 +1,171 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, newts } from "@server/db"; +import { siteResources, sites, orgs, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import { addTargets } from "../client/targets"; + +const createSiteResourceParamsSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +const createSiteResourceSchema = z + .object({ + name: z.string().min(1).max(255), + protocol: z.enum(["tcp", "udp"]), + proxyPort: z.number().int().positive(), + destinationPort: z.number().int().positive(), + destinationIp: z.string().ip(), + enabled: z.boolean().default(true) + }) + .strict(); + +export type CreateSiteResourceBody = z.infer; +export type CreateSiteResourceResponse = SiteResource; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/site/{siteId}/resource", + description: "Create a new site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: createSiteResourceParamsSchema, + body: { + content: { + "application/json": { + schema: createSiteResourceSchema + } + } + } + }, + responses: {} +}); + +export async function createSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = createSiteResourceParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = createSiteResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { siteId, orgId } = parsedParams.data; + const { + name, + protocol, + proxyPort, + destinationPort, + destinationIp, + enabled + } = parsedBody.data; + + // Verify the site exists and belongs to the org + const [site] = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (!site) { + return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); + } + + // check if resource with same protocol and proxy port already exists + const [existingResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId), + eq(siteResources.protocol, protocol), + eq(siteResources.proxyPort, proxyPort) + ) + ) + .limit(1); + if (existingResource && existingResource.siteResourceId) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A resource with the same protocol and proxy port already exists" + ) + ); + } + + // Create the site resource + const [newSiteResource] = await db + .insert(siteResources) + .values({ + siteId, + orgId, + name, + protocol, + proxyPort, + destinationPort, + destinationIp, + enabled + }) + .returning(); + + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await addTargets(newt.newtId, destinationIp, destinationPort, protocol); + + logger.info( + `Created site resource ${newSiteResource.siteResourceId} for site ${siteId}` + ); + + return response(res, { + data: newSiteResource, + success: true, + error: false, + message: "Site resource created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error("Error creating site resource:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create site resource" + ) + ); + } +} diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts new file mode 100644 index 00000000..df29faf5 --- /dev/null +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -0,0 +1,124 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, newts, sites } from "@server/db"; +import { siteResources } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import { removeTargets } from "../client/targets"; + +const deleteSiteResourceParamsSchema = z + .object({ + siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +export type DeleteSiteResourceResponse = { + message: string; +}; + +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + description: "Delete a site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: deleteSiteResourceParamsSchema + }, + responses: {} +}); + +export async function deleteSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = deleteSiteResourceParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { siteResourceId, siteId, orgId } = parsedParams.data; + + const [site] = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (!site) { + return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); + } + + // Check if site resource exists + const [existingSiteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + + if (!existingSiteResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site resource not found" + ) + ); + } + + // Delete the site resource + await db + .delete(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )); + + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await removeTargets( + newt.newtId, + existingSiteResource.destinationIp, + existingSiteResource.destinationPort, + existingSiteResource.protocol + ); + + logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`); + + return response(res, { + data: { message: "Site resource deleted successfully" }, + success: true, + error: false, + message: "Site resource deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error deleting site resource:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to delete site resource")); + } +} diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts new file mode 100644 index 00000000..914706cd --- /dev/null +++ b/server/routers/siteResource/getSiteResource.ts @@ -0,0 +1,83 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { siteResources, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; + +const getSiteResourceParamsSchema = z + .object({ + siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +export type GetSiteResourceResponse = SiteResource; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + description: "Get a specific site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: getSiteResourceParamsSchema + }, + responses: {} +}); + +export async function getSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getSiteResourceParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { siteResourceId, siteId, orgId } = parsedParams.data; + + // Get the site resource + const [siteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + + if (!siteResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site resource not found" + ) + ); + } + + return response(res, { + data: siteResource, + success: true, + error: false, + message: "Site resource retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error getting site resource:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to get site resource")); + } +} diff --git a/server/routers/siteResource/index.ts b/server/routers/siteResource/index.ts new file mode 100644 index 00000000..2c3e2526 --- /dev/null +++ b/server/routers/siteResource/index.ts @@ -0,0 +1,6 @@ +export * from "./createSiteResource"; +export * from "./deleteSiteResource"; +export * from "./getSiteResource"; +export * from "./updateSiteResource"; +export * from "./listSiteResources"; +export * from "./listAllSiteResourcesByOrg"; diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts new file mode 100644 index 00000000..948fc2c2 --- /dev/null +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -0,0 +1,111 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { siteResources, sites, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listAllSiteResourcesByOrgParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const listAllSiteResourcesByOrgQuerySchema = z.object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +export type ListAllSiteResourcesByOrgResponse = { + siteResources: (SiteResource & { siteName: string, siteNiceId: string })[]; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site-resources", + description: "List all site resources for an organization.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: listAllSiteResourcesByOrgParamsSchema, + query: listAllSiteResourcesByOrgQuerySchema + }, + responses: {} +}); + +export async function listAllSiteResourcesByOrg( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = listAllSiteResourcesByOrgParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedQuery = listAllSiteResourcesByOrgQuerySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const { limit, offset } = parsedQuery.data; + + // Get all site resources for the org with site names + const siteResourcesList = await db + .select({ + siteResourceId: siteResources.siteResourceId, + siteId: siteResources.siteId, + orgId: siteResources.orgId, + name: siteResources.name, + protocol: siteResources.protocol, + proxyPort: siteResources.proxyPort, + destinationPort: siteResources.destinationPort, + destinationIp: siteResources.destinationIp, + enabled: siteResources.enabled, + siteName: sites.name, + siteNiceId: sites.niceId + }) + .from(siteResources) + .innerJoin(sites, eq(siteResources.siteId, sites.siteId)) + .where(eq(siteResources.orgId, orgId)) + .limit(limit) + .offset(offset); + + return response(res, { + data: { siteResources: siteResourcesList }, + success: true, + error: false, + message: "Site resources retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error listing all site resources by org:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); + } +} diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts new file mode 100644 index 00000000..7fdb7a85 --- /dev/null +++ b/server/routers/siteResource/listSiteResources.ts @@ -0,0 +1,118 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { siteResources, sites, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listSiteResourcesParamsSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +const listSiteResourcesQuerySchema = z.object({ + limit: z + .string() + .optional() + .default("100") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +export type ListSiteResourcesResponse = { + siteResources: SiteResource[]; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site/{siteId}/resources", + description: "List site resources for a site.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: listSiteResourcesParamsSchema, + query: listSiteResourcesQuerySchema + }, + responses: {} +}); + +export async function listSiteResources( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = listSiteResourcesParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedQuery = listSiteResourcesQuerySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { siteId, orgId } = parsedParams.data; + const { limit, offset } = parsedQuery.data; + + // Verify the site exists and belongs to the org + const site = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (site.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site not found" + ) + ); + } + + // Get site resources + const siteResourcesList = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(limit) + .offset(offset); + + return response(res, { + data: { siteResources: siteResourcesList }, + success: true, + error: false, + message: "Site resources retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error listing site resources:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); + } +} diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts new file mode 100644 index 00000000..bd717463 --- /dev/null +++ b/server/routers/siteResource/updateSiteResource.ts @@ -0,0 +1,196 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, newts, sites } from "@server/db"; +import { siteResources, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import { addTargets } from "../client/targets"; + +const updateSiteResourceParamsSchema = z + .object({ + siteResourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +const updateSiteResourceSchema = z + .object({ + name: z.string().min(1).max(255).optional(), + protocol: z.enum(["tcp", "udp"]).optional(), + proxyPort: z.number().int().positive().optional(), + destinationPort: z.number().int().positive().optional(), + destinationIp: z.string().ip().optional(), + enabled: z.boolean().optional() + }) + .strict(); + +export type UpdateSiteResourceBody = z.infer; +export type UpdateSiteResourceResponse = SiteResource; + +registry.registerPath({ + method: "post", + path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + description: "Update a site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: updateSiteResourceParamsSchema, + body: { + content: { + "application/json": { + schema: updateSiteResourceSchema + } + } + } + }, + responses: {} +}); + +export async function updateSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = updateSiteResourceParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = updateSiteResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { siteResourceId, siteId, orgId } = parsedParams.data; + const updateData = parsedBody.data; + + const [site] = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (!site) { + return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); + } + + // Check if site resource exists + const [existingSiteResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) + ) + .limit(1); + + if (!existingSiteResource) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Site resource not found") + ); + } + + const protocol = updateData.protocol || existingSiteResource.protocol; + const proxyPort = + updateData.proxyPort || existingSiteResource.proxyPort; + + // check if resource with same protocol and proxy port already exists + const [existingResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId), + eq(siteResources.protocol, protocol), + eq(siteResources.proxyPort, proxyPort) + ) + ) + .limit(1); + if ( + existingResource && + existingResource.siteResourceId !== siteResourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A resource with the same protocol and proxy port already exists" + ) + ); + } + + // Update the site resource + const [updatedSiteResource] = await db + .update(siteResources) + .set(updateData) + .where( + and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) + ) + .returning(); + + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await addTargets( + newt.newtId, + updatedSiteResource.destinationIp, + updatedSiteResource.destinationPort, + updatedSiteResource.protocol + ); + + logger.info( + `Updated site resource ${siteResourceId} for site ${siteId}` + ); + + return response(res, { + data: updatedSiteResource, + success: true, + error: false, + message: "Site resource updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error updating site resource:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to update site resource" + ) + ); + } +} diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index ffea1571..7a3acd55 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -26,6 +26,7 @@ const createTargetParamsSchema = z const createTargetSchema = z .object({ + siteId: z.number().int().positive(), ip: z.string().refine(isTargetValid), method: z.string().optional().nullable(), port: z.number().int().min(1).max(65535), @@ -98,17 +99,41 @@ export async function createTarget( ); } + const siteId = targetData.siteId; + const [site] = await db .select() .from(sites) - .where(eq(sites.siteId, resource.siteId!)) + .where(eq(sites.siteId, siteId)) .limit(1); if (!site) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${resource.siteId} not found` + `Site with ID ${siteId} not found` + ) + ); + } + + const existingTargets = await db + .select() + .from(targets) + .where(eq(targets.resourceId, resourceId)); + + const existingTarget = existingTargets.find( + (target) => + target.ip === targetData.ip && + target.port === targetData.port && + target.method === targetData.method && + target.siteId === targetData.siteId + ); + + if (existingTarget) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}` ) ); } @@ -173,7 +198,12 @@ export async function createTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, newTarget, resource.protocol, resource.proxyPort); + await addTargets( + newt.newtId, + newTarget, + resource.protocol, + resource.proxyPort + ); } } } diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 6eadeccd..596691e4 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -76,38 +76,38 @@ export async function deleteTarget( ); } - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, resource.siteId!)) - .limit(1); - - if (!site) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${resource.siteId} not found` - ) - ); - } - - if (site.pubKey) { - if (site.type == "wireguard") { - await addPeer(site.exitNodeId!, { - publicKey: site.pubKey, - allowedIps: await getAllowedIps(site.siteId) - }); - } else if (site.type == "newt") { - // get the newt on the site by querying the newt table for siteId - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort); - } - } + // const [site] = await db + // .select() + // .from(sites) + // .where(eq(sites.siteId, resource.siteId!)) + // .limit(1); + // + // if (!site) { + // return next( + // createHttpError( + // HttpCode.NOT_FOUND, + // `Site with ID ${resource.siteId} not found` + // ) + // ); + // } + // + // if (site.pubKey) { + // if (site.type == "wireguard") { + // await addPeer(site.exitNodeId!, { + // publicKey: site.pubKey, + // allowedIps: await getAllowedIps(site.siteId) + // }); + // } else if (site.type == "newt") { + // // get the newt on the site by querying the newt table for siteId + // const [newt] = await db + // .select() + // .from(newts) + // .where(eq(newts.siteId, site.siteId)) + // .limit(1); + // + // removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort); + // } + // } return response(res, { data: null, diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 071ec8a6..b0691087 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, Target } from "@server/db"; import { targets } from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; @@ -16,6 +16,8 @@ const getTargetSchema = z }) .strict(); +type GetTargetResponse = Target; + registry.registerPath({ method: "get", path: "/target/{targetId}", @@ -60,7 +62,7 @@ export async function getTarget( ); } - return response(res, { + return response(res, { data: target[0], success: true, error: false, diff --git a/server/routers/target/helpers.ts b/server/routers/target/helpers.ts index e5aa2ba9..4935d28a 100644 --- a/server/routers/target/helpers.ts +++ b/server/routers/target/helpers.ts @@ -8,29 +8,21 @@ export async function pickPort(siteId: number): Promise<{ internalPort: number; targetIps: string[]; }> { - const resourcesRes = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - // TODO: is this all inefficient? // Fetch targets for all resources of this site const targetIps: string[] = []; const targetInternalPorts: number[] = []; - await Promise.all( - resourcesRes.map(async (resource) => { - const targetsRes = await db - .select() - .from(targets) - .where(eq(targets.resourceId, resource.resourceId)); - targetsRes.forEach((target) => { - targetIps.push(`${target.ip}/32`); - if (target.internalPort) { - targetInternalPorts.push(target.internalPort); - } - }); - }) - ); + + const targetsRes = await db + .select() + .from(targets) + .where(eq(targets.siteId, siteId)); + + targetsRes.forEach((target) => { + targetIps.push(`${target.ip}/32`); + if (target.internalPort) { + targetInternalPorts.push(target.internalPort); + } + }); let internalPort!: number; // pick a port random port from 40000 to 65535 that is not in use @@ -43,28 +35,20 @@ export async function pickPort(siteId: number): Promise<{ break; } } + currentBannedPorts.push(internalPort); return { internalPort, targetIps }; } export async function getAllowedIps(siteId: number) { - // TODO: is this all inefficient? - - const resourcesRes = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - // Fetch targets for all resources of this site - const targetIps = await Promise.all( - resourcesRes.map(async (resource) => { - const targetsRes = await db - .select() - .from(targets) - .where(eq(targets.resourceId, resource.resourceId)); - return targetsRes.map((target) => `${target.ip}/32`); - }) - ); + const targetsRes = await db + .select() + .from(targets) + .where(eq(targets.siteId, siteId)); + + const targetIps = targetsRes.map((target) => `${target.ip}/32`); + return targetIps.flat(); } diff --git a/server/routers/target/index.ts b/server/routers/target/index.ts index b128edcd..dc1323f7 100644 --- a/server/routers/target/index.ts +++ b/server/routers/target/index.ts @@ -2,4 +2,4 @@ export * from "./getTarget"; export * from "./createTarget"; export * from "./deleteTarget"; export * from "./updateTarget"; -export * from "./listTargets"; \ No newline at end of file +export * from "./listTargets"; diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 44f27d48..eab8f1c8 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -1,4 +1,4 @@ -import { db } from "@server/db"; +import { db, sites } from "@server/db"; import { targets } from "@server/db"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -42,11 +42,12 @@ function queryTargets(resourceId: number) { method: targets.method, port: targets.port, enabled: targets.enabled, - resourceId: targets.resourceId - // resourceName: resources.name, + resourceId: targets.resourceId, + siteId: targets.siteId, + siteType: sites.type }) .from(targets) - // .leftJoin(resources, eq(targets.resourceId, resources.resourceId)) + .leftJoin(sites, eq(sites.siteId, targets.siteId)) .where(eq(targets.resourceId, resourceId)); return baseQuery; diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 0b7c4692..67d9a8df 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -22,6 +22,7 @@ const updateTargetParamsSchema = z const updateTargetBodySchema = z .object({ + siteId: z.number().int().positive(), ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), port: z.number().int().min(1).max(65535).optional(), @@ -77,6 +78,7 @@ export async function updateTarget( } const { targetId } = parsedParams.data; + const { siteId } = parsedBody.data; const [target] = await db .select() @@ -111,14 +113,42 @@ export async function updateTarget( const [site] = await db .select() .from(sites) - .where(eq(sites.siteId, resource.siteId!)) + .where(eq(sites.siteId, siteId)) .limit(1); if (!site) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${resource.siteId} not found` + `Site with ID ${siteId} not found` + ) + ); + } + + const targetData = { + ...target, + ...parsedBody.data + }; + + const existingTargets = await db + .select() + .from(targets) + .where(eq(targets.resourceId, target.resourceId)); + + const foundTarget = existingTargets.find( + (target) => + target.targetId !== targetId && // Exclude the current target being updated + target.ip === targetData.ip && + target.port === targetData.port && + target.method === targetData.method && + target.siteId === targetData.siteId + ); + + if (foundTarget) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Target with IP ${targetData.ip}, port ${targetData.port}, and method ${targetData.method} already exists on the same site.` ) ); } @@ -157,7 +187,12 @@ export async function updateTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, [updatedTarget], resource.protocol, resource.proxyPort); + await addTargets( + newt.newtId, + [updatedTarget], + resource.protocol, + resource.proxyPort + ); } } return response(res, { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 882a296a..e3b62176 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -1,11 +1,21 @@ import { Request, Response } from "express"; import { db, exitNodes } from "@server/db"; -import { and, eq, inArray, or, isNull } from "drizzle-orm"; +import { and, eq, inArray, or, isNull, ne } from "drizzle-orm"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import config from "@server/lib/config"; import { orgs, resources, sites, Target, targets } from "@server/db"; +// Extended Target interface that includes site information +interface TargetWithSite extends Target { + site: { + siteId: number; + type: string; + subnet: string | null; + exitNodeId: number | null; + }; +} + let currentExitNodeId: number; export async function traefikConfigProvider( @@ -44,8 +54,9 @@ export async function traefikConfigProvider( } } - // Get the site(s) on this exit node - const resourcesWithRelations = await tx + // Get resources with their targets and sites in a single optimized query + // Start from sites on this exit node, then join to targets and resources + const resourcesWithTargetsAndSites = await tx .select({ // Resource fields resourceId: resources.resourceId, @@ -56,67 +67,82 @@ export async function traefikConfigProvider( protocol: resources.protocol, subdomain: resources.subdomain, domainId: resources.domainId, - // Site fields - site: { - siteId: sites.siteId, - type: sites.type, - subnet: sites.subnet, - exitNodeId: sites.exitNodeId - }, enabled: resources.enabled, stickySession: resources.stickySession, tlsServerName: resources.tlsServerName, setHostHeader: resources.setHostHeader, - enableProxy: resources.enableProxy + enableProxy: resources.enableProxy, + // Target fields + targetId: targets.targetId, + targetEnabled: targets.enabled, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + // Site fields + siteId: sites.siteId, + siteType: sites.type, + subnet: sites.subnet, + exitNodeId: sites.exitNodeId }) - .from(resources) - .innerJoin(sites, eq(sites.siteId, resources.siteId)) + .from(sites) + .innerJoin(targets, eq(targets.siteId, sites.siteId)) + .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) .where( - or( - eq(sites.exitNodeId, currentExitNodeId), - isNull(sites.exitNodeId) + and( + eq(targets.enabled, true), + eq(resources.enabled, true), + or( + eq(sites.exitNodeId, currentExitNodeId), + isNull(sites.exitNodeId) + ) ) ); - // Get all resource IDs from the first query - const resourceIds = resourcesWithRelations.map((r) => r.resourceId); + // Group by resource and include targets with their unique site data + const resourcesMap = new Map(); - // Second query to get all enabled targets for these resources - const allTargets = - resourceIds.length > 0 - ? await tx - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled - }) - .from(targets) - .where( - and( - inArray(targets.resourceId, resourceIds), - eq(targets.enabled, true) - ) - ) - : []; + resourcesWithTargetsAndSites.forEach((row) => { + const resourceId = row.resourceId; - // Create a map for fast target lookup by resourceId - const targetsMap = allTargets.reduce((map, target) => { - if (!map.has(target.resourceId)) { - map.set(target.resourceId, []); + if (!resourcesMap.has(resourceId)) { + resourcesMap.set(resourceId, { + resourceId: row.resourceId, + fullDomain: row.fullDomain, + ssl: row.ssl, + http: row.http, + proxyPort: row.proxyPort, + protocol: row.protocol, + subdomain: row.subdomain, + domainId: row.domainId, + enabled: row.enabled, + stickySession: row.stickySession, + tlsServerName: row.tlsServerName, + setHostHeader: row.setHostHeader, + enableProxy: row.enableProxy, + targets: [] + }); } - map.get(target.resourceId).push(target); - return map; - }, new Map()); - // Combine the data - return resourcesWithRelations.map((resource) => ({ - ...resource, - targets: targetsMap.get(resource.resourceId) || [] - })); + // Add target with its associated site data + resourcesMap.get(resourceId).targets.push({ + resourceId: row.resourceId, + targetId: row.targetId, + ip: row.ip, + method: row.method, + port: row.port, + internalPort: row.internalPort, + enabled: row.targetEnabled, + site: { + siteId: row.siteId, + type: row.siteType, + subnet: row.subnet, + exitNodeId: row.exitNodeId + } + }); + }); + + return Array.from(resourcesMap.values()); }); if (!allResources.length) { @@ -167,8 +193,7 @@ export async function traefikConfigProvider( }; for (const resource of allResources) { - const targets = resource.targets as Target[]; - const site = resource.site; + const targets = resource.targets as TargetWithSite[]; const routerName = `${resource.resourceId}-router`; const serviceName = `${resource.resourceId}-service`; @@ -272,13 +297,13 @@ export async function traefikConfigProvider( config_output.http.services![serviceName] = { loadBalancer: { servers: targets - .filter((target: Target) => { + .filter((target: TargetWithSite) => { if (!target.enabled) { return false; } if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { if ( !target.ip || @@ -287,27 +312,27 @@ export async function traefikConfigProvider( ) { return false; } - } else if (site.type === "newt") { + } else if (target.site.type === "newt") { if ( !target.internalPort || !target.method || - !site.subnet + !target.site.subnet ) { return false; } } return true; }) - .map((target: Target) => { + .map((target: TargetWithSite) => { if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { return { url: `${target.method}://${target.ip}:${target.port}` }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; + } else if (target.site.type === "newt") { + const ip = target.site.subnet!.split("/")[0]; return { url: `${target.method}://${ip}:${target.internalPort}` }; @@ -393,34 +418,34 @@ export async function traefikConfigProvider( config_output[protocol].services[serviceName] = { loadBalancer: { servers: targets - .filter((target: Target) => { + .filter((target: TargetWithSite) => { if (!target.enabled) { return false; } if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { if (!target.ip || !target.port) { return false; } - } else if (site.type === "newt") { - if (!target.internalPort || !site.subnet) { + } else if (target.site.type === "newt") { + if (!target.internalPort || !target.site.subnet) { return false; } } return true; }) - .map((target: Target) => { + .map((target: TargetWithSite) => { if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { return { address: `${target.ip}:${target.port}` }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; + } else if (target.site.type === "newt") { + const ip = target.site.subnet!.split("/")[0]; return { address: `${ip}:${target.internalPort}` }; diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts index c55d5463..f094e20e 100644 --- a/server/routers/user/addUserSite.ts +++ b/server/routers/user/addUserSite.ts @@ -43,17 +43,17 @@ export async function addUserSite( }) .returning(); - const siteResources = await trx - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx.insert(userResources).values({ - userId, - resourceId: resource.resourceId - }); - } + // const siteResources = await trx + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx.insert(userResources).values({ + // userId, + // resourceId: resource.resourceId + // }); + // } return response(res, { data: newUserSite[0], diff --git a/server/routers/user/removeUserSite.ts b/server/routers/user/removeUserSite.ts index 200999fd..7dbb4a15 100644 --- a/server/routers/user/removeUserSite.ts +++ b/server/routers/user/removeUserSite.ts @@ -71,22 +71,22 @@ export async function removeUserSite( ); } - const siteResources = await trx - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx - .delete(userResources) - .where( - and( - eq(userResources.userId, userId), - eq(userResources.resourceId, resource.resourceId) - ) - ) - .returning(); - } + // const siteResources = await trx + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx + // .delete(userResources) + // .where( + // and( + // eq(userResources.userId, userId), + // eq(userResources.resourceId, resource.resourceId) + // ) + // ) + // .returning(); + // } }); return response(res, { diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index d85cc277..05faece3 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -23,7 +23,7 @@ export const messageHandlers: Record = { "olm/ping": handleOlmPingMessage, "newt/socket/status": handleDockerStatusMessage, "newt/socket/containers": handleDockerContainersMessage, - "newt/ping/request": handleNewtPingRequestMessage, + "newt/ping/request": handleNewtPingRequestMessage }; startOfflineChecker(); // this is to handle the offline check for olms diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts index 22259cae..a12f5617 100644 --- a/server/setup/scriptsPg/1.9.0.ts +++ b/server/setup/scriptsPg/1.9.0.ts @@ -22,4 +22,4 @@ export default async function migration() { console.log("Unable to add setupTokens table:", e); throw e; } -} \ No newline at end of file +} diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts index a4a20dda..83dbf9d0 100644 --- a/server/setup/scriptsSqlite/1.9.0.ts +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -32,4 +32,4 @@ export default async function migration() { console.log("Unable to add setupTokens table:", e); throw e; } -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx deleted file mode 100644 index a675213a..00000000 --- a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "@app/components/ui/data-table"; -import { useTranslations } from 'next-intl'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - createResource?: () => void; -} - -export function ResourcesDataTable({ - columns, - data, - createResource -}: DataTableProps) { - - const t = useTranslations(); - - return ( - - ); -} diff --git a/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx b/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx deleted file mode 100644 index 50f6fd0b..00000000 --- a/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Server, Lock, Key, Users, X, ArrowRight } from "lucide-react"; // Replace with actual imports -import { Card, CardContent } from "@app/components/ui/card"; -import { Button } from "@app/components/ui/button"; -import { useTranslations } from "next-intl"; - -export const ResourcesSplashCard = () => { - const [isDismissed, setIsDismissed] = useState(false); - - const key = "resources-splash-dismissed"; - - useEffect(() => { - const dismissed = localStorage.getItem(key); - if (dismissed === "true") { - setIsDismissed(true); - } - }, []); - - const handleDismiss = () => { - setIsDismissed(true); - localStorage.setItem(key, "true"); - }; - - const t = useTranslations(); - - if (isDismissed) { - return null; - } - - return ( - - - -

-

- - {t('resources')} -

-

- {t('resourcesDescription')} -

-
    -
  • - - {t('resourcesWireGuardConnect')} -
  • -
  • - - {t('resourcesMultipleAuthenticationMethods')} -
  • -
  • - - {t('resourcesUsersRolesAccess')} -
  • -
-
- - - ); -}; - -export default ResourcesSplashCard; diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index e64fb4e3..a4209bee 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -1,7 +1,16 @@ "use client"; -import { ColumnDef } from "@tanstack/react-table"; -import { ResourcesDataTable } from "./ResourcesDataTable"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + SortingState, + getSortedRowModel, + ColumnFiltersState, + getFilteredRowModel +} from "@tanstack/react-table"; import { DropdownMenu, DropdownMenuContent, @@ -10,18 +19,16 @@ import { } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { - Copy, ArrowRight, ArrowUpDown, MoreHorizontal, - Check, ArrowUpRight, ShieldOff, ShieldCheck } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { formatAxiosError } from "@app/lib/api"; import { toast } from "@app/hooks/useToast"; @@ -31,17 +38,37 @@ import CopyToClipboard from "@app/components/CopyToClipboard"; import { Switch } from "@app/components/ui/switch"; import { AxiosResponse } from "axios"; import { UpdateResourceResponse } from "@server/routers/resource"; +import { ListSitesResponse } from "@server/routers/site"; import { useTranslations } from "next-intl"; import { InfoPopup } from "@app/components/ui/info-popup"; -import { Badge } from "@app/components/ui/badge"; +import { Input } from "@app/components/ui/input"; +import { DataTablePagination } from "@app/components/DataTablePagination"; +import { Plus, Search } from "lucide-react"; +import { Card, CardContent, CardHeader } from "@app/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger +} from "@app/components/ui/tabs"; +import { useSearchParams } from "next/navigation"; +import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog"; +import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; export type ResourceRow = { id: number; name: string; orgId: string; domain: string; - site: string; - siteId: string; authState: string; http: boolean; protocol: string; @@ -50,20 +77,147 @@ export type ResourceRow = { domainId?: string; }; -type ResourcesTableProps = { - resources: ResourceRow[]; +export type InternalResourceRow = { + id: number; + name: string; orgId: string; + siteName: string; + protocol: string; + proxyPort: number | null; + siteId: number; + siteNiceId: string; + destinationIp: string; + destinationPort: number; }; -export default function SitesTable({ resources, orgId }: ResourcesTableProps) { +type Site = ListSitesResponse["sites"][0]; + +type ResourcesTableProps = { + resources: ResourceRow[]; + internalResources: InternalResourceRow[]; + orgId: string; + defaultView?: "proxy" | "internal"; +}; + +export default function SitesTable({ + resources, + internalResources, + orgId, + defaultView = "proxy" +}: ResourcesTableProps) { const router = useRouter(); + const searchParams = useSearchParams(); const t = useTranslations(); - const api = createApiClient(useEnvContext()); + const { env } = useEnvContext(); + + const api = createApiClient({ env }); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedResource, setSelectedResource] = useState(); + const [selectedInternalResource, setSelectedInternalResource] = + useState(); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [editingResource, setEditingResource] = + useState(); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [sites, setSites] = useState([]); + + const [proxySorting, setProxySorting] = useState([]); + const [proxyColumnFilters, setProxyColumnFilters] = + useState([]); + const [proxyGlobalFilter, setProxyGlobalFilter] = useState([]); + + const [internalSorting, setInternalSorting] = useState([]); + const [internalColumnFilters, setInternalColumnFilters] = + useState([]); + const [internalGlobalFilter, setInternalGlobalFilter] = useState([]); + + const currentView = searchParams.get("view") || defaultView; + + useEffect(() => { + const fetchSites = async () => { + try { + const res = await api.get>( + `/org/${orgId}/sites` + ); + setSites(res.data.data.sites); + } catch (error) { + console.error("Failed to fetch sites:", error); + } + }; + + if (orgId) { + fetchSites(); + } + }, [orgId]); + + const handleTabChange = (value: string) => { + const params = new URLSearchParams(searchParams); + if (value === "internal") { + params.set("view", "internal"); + } else { + params.delete("view"); + } + + const newUrl = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`; + router.replace(newUrl, { scroll: false }); + }; + + const getSearchInput = () => { + if (currentView === "internal") { + return ( +
+ + internalTable.setGlobalFilter( + String(e.target.value) + ) + } + className="w-full pl-8" + /> + +
+ ); + } + return ( +
+ + proxyTable.setGlobalFilter(String(e.target.value)) + } + className="w-full pl-8" + /> + +
+ ); + }; + + const getActionButton = () => { + if (currentView === "internal") { + return ( + + ); + } + return ( + + ); + }; const deleteResource = (resourceId: number) => { api.delete(`/resource/${resourceId}`) @@ -81,6 +235,26 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { }); }; + const deleteInternalResource = async ( + resourceId: number, + siteId: number + ) => { + try { + await api.delete( + `/org/${orgId}/site/${siteId}/resource/${resourceId}` + ); + router.refresh(); + setIsDeleteModalOpen(false); + } catch (e) { + console.error(t("resourceErrorDelete"), e); + toast({ + variant: "destructive", + title: t("resourceErrorDelte"), + description: formatAxiosError(e, t("v")) + }); + } + }; + async function toggleResourceEnabled(val: boolean, resourceId: number) { const res = await api .post>( @@ -101,7 +275,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { }); } - const columns: ColumnDef[] = [ + const proxyColumns: ColumnDef[] = [ { accessorKey: "name", header: ({ column }) => { @@ -118,35 +292,6 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { ); } }, - { - accessorKey: "site", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const resourceRow = row.original; - return ( - - - - ); - } - }, { accessorKey: "protocol", header: t("protocol"), @@ -225,10 +370,12 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { toggleResourceEnabled(val, row.original.id) } @@ -289,6 +436,163 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { } ]; + const internalColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "siteName", + header: t("siteName"), + cell: ({ row }) => { + const resourceRow = row.original; + return ( + + + + ); + } + }, + { + accessorKey: "protocol", + header: t("protocol"), + cell: ({ row }) => { + const resourceRow = row.original; + return {resourceRow.protocol.toUpperCase()}; + } + }, + { + accessorKey: "proxyPort", + header: t("proxyPort"), + cell: ({ row }) => { + const resourceRow = row.original; + return ( + + ); + } + }, + { + accessorKey: "destination", + header: t("resourcesTableDestination"), + cell: ({ row }) => { + const resourceRow = row.original; + const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`; + return ; + } + }, + + { + id: "actions", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ + + + + + { + setSelectedInternalResource( + resourceRow + ); + setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + + + + + +
+ ); + } + } + ]; + + const proxyTable = useReactTable({ + data: resources, + columns: proxyColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setProxySorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setProxyColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onGlobalFilterChange: setProxyGlobalFilter, + initialState: { + pagination: { + pageSize: 20, + pageIndex: 0 + } + }, + state: { + sorting: proxySorting, + columnFilters: proxyColumnFilters, + globalFilter: proxyGlobalFilter + } + }); + + const internalTable = useReactTable({ + data: internalResources, + columns: internalColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setInternalSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setInternalColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onGlobalFilterChange: setInternalGlobalFilter, + initialState: { + pagination: { + pageSize: 20, + pageIndex: 0 + } + }, + state: { + sorting: internalSorting, + columnFilters: internalColumnFilters, + globalFilter: internalGlobalFilter + } + }); + return ( <> {selectedResource && ( @@ -320,11 +624,271 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { /> )} - { - router.push(`/${orgId}/settings/resources/create`); + {selectedInternalResource && ( + { + setIsDeleteModalOpen(val); + setSelectedInternalResource(null); + }} + dialog={ +
+

+ {t("resourceQuestionRemove", { + selectedResource: + selectedInternalResource?.name || + selectedInternalResource?.id + })} +

+ +

{t("resourceMessageRemove")}

+ +

{t("resourceMessageConfirm")}

+
+ } + buttonText={t("resourceDeleteConfirm")} + onConfirm={async () => + deleteInternalResource( + selectedInternalResource!.id, + selectedInternalResource!.siteId + ) + } + string={selectedInternalResource.name} + title={t("resourceDelete")} + /> + )} + +
+ + + +
+ {getSearchInput()} + + {env.flags.enableClients && ( + + + {t("resourcesTableProxyResources")} + + + {t("resourcesTableClientResources")} + + + )} +
+
+ {getActionButton()} +
+
+ + + + + {proxyTable + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} + + + {proxyTable.getRowModel().rows + ?.length ? ( + proxyTable + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {t( + "resourcesTableNoProxyResourcesFound" + )} + + + )} + +
+
+ +
+
+ +
+ + + {t( + "resourcesTableTheseResourcesForUseWith" + )}{" "} + + {t("resourcesTableClients")} + + {" "} + {t( + "resourcesTableAndOnlyAccessibleInternally" + )} + + +
+ + + {internalTable + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} + + + {internalTable.getRowModel().rows + ?.length ? ( + internalTable + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {t( + "resourcesTableNoInternalResourcesFound" + )} + + + )} + +
+
+ +
+
+
+
+
+
+ + {editingResource && ( + { + router.refresh(); + setEditingResource(null); + }} + /> + )} + + { + router.refresh(); }} /> diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx index 68331ff9..af7d96fc 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx @@ -10,35 +10,22 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useDockerSocket } from "@app/hooks/useDockerSocket"; import { useTranslations } from "next-intl"; -import { AxiosResponse } from "axios"; -import { useEffect, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { RotateCw } from "lucide-react"; -import { createApiClient } from "@app/lib/api"; import { build } from "@server/build"; type ResourceInfoBoxType = {}; export default function ResourceInfoBox({}: ResourceInfoBoxType) { - const { resource, authInfo, site } = useResourceContext(); - const api = createApiClient(useEnvContext()); + const { resource, authInfo } = useResourceContext(); - const { isEnabled, isAvailable } = useDockerSocket(site!); const t = useTranslations(); const fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; return ( - - - {t("resourceInfo")} - - - + + {resource.http ? ( <> @@ -71,12 +58,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { /> - - {t("site")} - - {resource.siteName} - - {/* {isEnabled && ( Socket @@ -117,7 +98,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { /> - {build == "oss" && ( + {/* {build == "oss" && ( {t("externalProxyEnabled")} @@ -130,7 +111,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { - )} + )} */} )} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index c8f6255c..9bb9919a 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -49,6 +49,15 @@ import { UserType } from "@server/types/UserTypes"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; import { useTranslations } from "next-intl"; +import { CheckboxWithLabel } from "@app/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { Separator } from "@app/components/ui/separator"; const UsersRolesFormSchema = z.object({ roles: z.array( @@ -110,6 +119,14 @@ export default function ResourceAuthenticationPage() { resource.emailWhitelistEnabled ); + const [autoLoginEnabled, setAutoLoginEnabled] = useState( + resource.skipToIdpId !== null && resource.skipToIdpId !== undefined + ); + const [selectedIdpId, setSelectedIdpId] = useState( + resource.skipToIdpId || null + ); + const [allIdps, setAllIdps] = useState<{ id: number; text: string }[]>([]); + const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); const [loadingSaveWhitelist, setLoadingSaveWhitelist] = useState(false); @@ -139,7 +156,8 @@ export default function ResourceAuthenticationPage() { resourceRolesResponse, usersResponse, resourceUsersResponse, - whitelist + whitelist, + idpsResponse ] = await Promise.all([ api.get>( `/org/${org?.org.orgId}/roles` @@ -155,7 +173,12 @@ export default function ResourceAuthenticationPage() { ), api.get>( `/resource/${resource.resourceId}/whitelist` - ) + ), + api.get< + AxiosResponse<{ + idps: { idpId: number; name: string }[]; + }> + >("/idp") ]); setAllRoles( @@ -200,6 +223,21 @@ export default function ResourceAuthenticationPage() { })) ); + setAllIdps( + idpsResponse.data.data.idps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })) + ); + + if ( + autoLoginEnabled && + !selectedIdpId && + idpsResponse.data.data.idps.length > 0 + ) { + setSelectedIdpId(idpsResponse.data.data.idps[0].idpId); + } + setPageLoading(false); } catch (e) { console.error(e); @@ -260,6 +298,16 @@ export default function ResourceAuthenticationPage() { try { setLoadingSaveUsersRoles(true); + // Validate that an IDP is selected if auto login is enabled + if (autoLoginEnabled && !selectedIdpId) { + toast({ + variant: "destructive", + title: t("error"), + description: t("selectIdpRequired") + }); + return; + } + const jobs = [ api.post(`/resource/${resource.resourceId}/roles`, { roleIds: data.roles.map((i) => parseInt(i.id)) @@ -268,14 +316,16 @@ export default function ResourceAuthenticationPage() { userIds: data.users.map((i) => i.id) }), api.post(`/resource/${resource.resourceId}`, { - sso: ssoEnabled + sso: ssoEnabled, + skipToIdpId: autoLoginEnabled ? selectedIdpId : null }) ]; await Promise.all(jobs); updateResource({ - sso: ssoEnabled + sso: ssoEnabled, + skipToIdpId: autoLoginEnabled ? selectedIdpId : null }); updateAuthInfo({ @@ -542,6 +592,89 @@ export default function ResourceAuthenticationPage() { /> )} + + {ssoEnabled && allIdps.length > 0 && ( +
+
+ { + setAutoLoginEnabled( + checked as boolean + ); + if ( + checked && + allIdps.length > 0 + ) { + setSelectedIdpId( + allIdps[0].id + ); + } else { + setSelectedIdpId( + null + ); + } + }} + /> +

+ {t( + "autoLoginExternalIdpDescription" + )} +

+
+ + {autoLoginEnabled && ( +
+ + +
+ )} +
+ )} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index b4e14d64..8c5ee667 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -14,19 +14,6 @@ import { FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem -} from "@/components/ui/command"; -import { cn } from "@app/lib/cn"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@/components/ui/popover"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { ListSitesResponse } from "@server/routers/site"; import { useEffect, useState } from "react"; @@ -45,25 +32,11 @@ import { SettingsSectionFooter } from "@app/components/Settings"; import { useOrgContext } from "@app/hooks/useOrgContext"; -import CustomDomainInput from "../CustomDomainInput"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; -import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; -import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Label } from "@app/components/ui/label"; import { ListDomainsResponse } from "@server/routers/domain"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { - UpdateResourceResponse, - updateResourceRule -} from "@server/routers/resource"; +import { UpdateResourceResponse } from "@server/routers/resource"; import { SwitchInput } from "@app/components/SwitchInput"; import { useTranslations } from "next-intl"; import { Checkbox } from "@app/components/ui/checkbox"; @@ -81,12 +54,6 @@ import DomainPicker from "@app/components/DomainPicker"; import { Globe } from "lucide-react"; import { build } from "@server/build"; -const TransferFormSchema = z.object({ - siteId: z.number() -}); - -type TransferFormValues = z.infer; - export default function GeneralForm() { const [formKey, setFormKey] = useState(0); const params = useParams(); @@ -127,7 +94,7 @@ export default function GeneralForm() { name: z.string().min(1).max(255), domainId: z.string().optional(), proxyPort: z.number().int().min(1).max(65535).optional(), - enableProxy: z.boolean().optional() + // enableProxy: z.boolean().optional() }) .refine( (data) => { @@ -156,18 +123,11 @@ export default function GeneralForm() { subdomain: resource.subdomain ? resource.subdomain : undefined, domainId: resource.domainId || undefined, proxyPort: resource.proxyPort || undefined, - enableProxy: resource.enableProxy || false + // enableProxy: resource.enableProxy || false }, mode: "onChange" }); - const transferForm = useForm({ - resolver: zodResolver(TransferFormSchema), - defaultValues: { - siteId: resource.siteId ? Number(resource.siteId) : undefined - } - }); - useEffect(() => { const fetchSites = async () => { const res = await api.get>( @@ -221,9 +181,9 @@ export default function GeneralForm() { subdomain: data.subdomain, domainId: data.domainId, proxyPort: data.proxyPort, - ...(!resource.http && { - enableProxy: data.enableProxy - }) + // ...(!resource.http && { + // enableProxy: data.enableProxy + // }) } ) .catch((e) => { @@ -251,9 +211,9 @@ export default function GeneralForm() { subdomain: data.subdomain, fullDomain: resource.fullDomain, proxyPort: data.proxyPort, - ...(!resource.http && { - enableProxy: data.enableProxy - }), + // ...(!resource.http && { + // enableProxy: data.enableProxy + // }) }); router.refresh(); @@ -261,40 +221,6 @@ export default function GeneralForm() { setSaveLoading(false); } - async function onTransfer(data: TransferFormValues) { - setTransferLoading(true); - - const res = await api - .post(`resource/${resource?.resourceId}/transfer`, { - siteId: data.siteId - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorTransfer"), - description: formatAxiosError( - e, - t("resourceErrorTransferDescription") - ) - }); - }); - - if (res && res.status === 200) { - toast({ - title: t("resourceTransferred"), - description: t("resourceTransferredDescription") - }); - router.refresh(); - - updateResource({ - siteName: - sites.find((site) => site.siteId === data.siteId)?.name || - "" - }); - } - setTransferLoading(false); - } - return ( !loadingPage && ( <> @@ -410,7 +336,7 @@ export default function GeneralForm() { )} /> - {build == "oss" && ( + {/* {build == "oss" && ( )} /> - )} + )} */} )} {resource.http && (
- +
@@ -466,7 +394,9 @@ export default function GeneralForm() { ) } > - Edit Domain + {t( + "resourceEditDomain" + )}
@@ -490,140 +420,6 @@ export default function GeneralForm() { - - - - - {t("resourceTransfer")} - - - {t("resourceTransferDescription")} - - - - - -
- - ( - - - {t("siteDestination")} - - - - - - - - - - - - {t( - "sitesNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - transferForm.setValue( - "siteId", - site.siteId - ); - setOpen( - false - ); - }} - > - { - site.name - } - - - ) - )} - - - - - - - )} - /> - - -
-
- - - - -
>( `/resource/${params.resourceId}`, @@ -44,19 +43,6 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { redirect(`/${params.orgId}/settings/resources`); } - // Fetch site info - if (resource.siteId) { - try { - const res = await internal.get>( - `/site/${resource.siteId}`, - await authCookieHeader() - ); - site = res.data.data; - } catch { - redirect(`/${params.orgId}/settings/resources`); - } - } - try { const res = await internal.get< AxiosResponse @@ -119,7 +105,6 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 7ab02c7e..c6584219 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useState, use } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Select, SelectContent, @@ -34,12 +33,12 @@ import { getPaginationRowModel, getCoreRowModel, useReactTable, - flexRender + flexRender, + Row } from "@tanstack/react-table"; import { Table, TableBody, - TableCaption, TableCell, TableHead, TableHeader, @@ -51,7 +50,7 @@ import { ArrayElement } from "@server/types/ArrayElement"; import { formatAxiosError } from "@app/lib/api/formatAxiosError"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient } from "@app/lib/api"; -import { GetSiteResponse } from "@server/routers/site"; +import { GetSiteResponse, ListSitesResponse } from "@server/routers/site"; import { SettingsContainer, SettingsSection, @@ -59,28 +58,48 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm, - SettingsSectionGrid + SettingsSectionForm } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { useRouter } from "next/navigation"; import { isTargetValid } from "@server/lib/validators"; import { tlsNameSchema } from "@server/lib/schemas"; -import { ChevronsUpDown } from "lucide-react"; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger -} from "@app/components/ui/collapsible"; + CheckIcon, + ChevronsUpDown, + Settings, + Heart, + Check, + CircleCheck, + CircleX +} from "lucide-react"; import { ContainersSelector } from "@app/components/ContainersSelector"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; +import { DockerManager, DockerState } from "@app/lib/docker"; +import { Container } from "@server/routers/site"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { cn } from "@app/lib/cn"; +import { CaretSortIcon } from "@radix-ui/react-icons"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { Badge } from "@app/components/ui/badge"; const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), - port: z.coerce.number().int().positive() + port: z.coerce.number().int().positive(), + siteId: z.number().int().positive() }); const targetsSettingsSchema = z.object({ @@ -91,12 +110,13 @@ type LocalTarget = Omit< ArrayElement & { new?: boolean; updated?: boolean; + siteType: string | null; }, "protocol" >; export default function ReverseProxyTargets(props: { - params: Promise<{ resourceId: number }>; + params: Promise<{ resourceId: number; orgId: string }>; }) { const params = use(props.params); const t = useTranslations(); @@ -106,15 +126,48 @@ export default function ReverseProxyTargets(props: { const api = createApiClient(useEnvContext()); const [targets, setTargets] = useState([]); - const [site, setSite] = useState(); const [targetsToRemove, setTargetsToRemove] = useState([]); + const [sites, setSites] = useState([]); + const [dockerStates, setDockerStates] = useState>(new Map()); + + const initializeDockerForSite = async (siteId: number) => { + if (dockerStates.has(siteId)) { + return; // Already initialized + } + + const dockerManager = new DockerManager(api, siteId); + const dockerState = await dockerManager.initializeDocker(); + + setDockerStates(prev => new Map(prev.set(siteId, dockerState))); + }; + + const refreshContainersForSite = async (siteId: number) => { + const dockerManager = new DockerManager(api, siteId); + const containers = await dockerManager.fetchContainers(); + + setDockerStates(prev => { + const newMap = new Map(prev); + const existingState = newMap.get(siteId); + if (existingState) { + newMap.set(siteId, { ...existingState, containers }); + } + return newMap; + }); + }; + + const getDockerStateForSite = (siteId: number): DockerState => { + return dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + }; + }; const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); const [targetsLoading, setTargetsLoading] = useState(false); const [proxySettingsLoading, setProxySettingsLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); - const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const router = useRouter(); const proxySettingsSchema = z.object({ @@ -167,6 +220,14 @@ export default function ReverseProxyTargets(props: { const watchedIp = addTargetForm.watch("ip"); const watchedPort = addTargetForm.watch("port"); + const watchedSiteId = addTargetForm.watch("siteId"); + + const handleContainerSelect = (hostname: string, port?: number) => { + addTargetForm.setValue("ip", hostname); + if (port) { + addTargetForm.setValue("port", port); + } + }; const tlsSettingsForm = useForm({ resolver: zodResolver(tlsSettingsSchema), @@ -216,28 +277,64 @@ export default function ReverseProxyTargets(props: { }; fetchTargets(); - const fetchSite = async () => { - try { - const res = await api.get>( - `/site/${resource.siteId}` - ); - - if (res.status === 200) { - setSite(res.data.data); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("siteErrorFetch"), - description: formatAxiosError( - err, - t("siteErrorFetchDescription") - ) + const fetchSites = async () => { + const res = await api + .get< + AxiosResponse + >(`/org/${params.orgId}/sites`) + .catch((e) => { + toast({ + variant: "destructive", + title: t("sitesErrorFetch"), + description: formatAxiosError( + e, + t("sitesErrorFetchDescription") + ) + }); }); + + if (res?.status === 200) { + setSites(res.data.data.sites); + + // Initialize Docker for newt sites + const newtSites = res.data.data.sites.filter(site => site.type === "newt"); + for (const site of newtSites) { + initializeDockerForSite(site.siteId); + } + + // If there's only one site, set it as the default in the form + if (res.data.data.sites.length) { + addTargetForm.setValue( + "siteId", + res.data.data.sites[0].siteId + ); + } } }; - fetchSite(); + fetchSites(); + + // const fetchSite = async () => { + // try { + // const res = await api.get>( + // `/site/${resource.siteId}` + // ); + // + // if (res.status === 200) { + // setSite(res.data.data); + // } + // } catch (err) { + // console.error(err); + // toast({ + // variant: "destructive", + // title: t("siteErrorFetch"), + // description: formatAxiosError( + // err, + // t("siteErrorFetchDescription") + // ) + // }); + // } + // }; + // fetchSite(); }, []); async function addTarget(data: z.infer) { @@ -246,7 +343,8 @@ export default function ReverseProxyTargets(props: { (target) => target.ip === data.ip && target.port === data.port && - target.method === data.method + target.method === data.method && + target.siteId === data.siteId ); if (isDuplicate) { @@ -258,34 +356,37 @@ export default function ReverseProxyTargets(props: { return; } - if (site && site.type == "wireguard" && site.subnet) { - // make sure that the target IP is within the site subnet - const targetIp = data.ip; - const subnet = site.subnet; - try { - if (!isIPInSubnet(targetIp, subnet)) { - toast({ - variant: "destructive", - title: t("targetWireGuardErrorInvalidIp"), - description: t( - "targetWireGuardErrorInvalidIpDescription" - ) - }); - return; - } - } catch (error) { - console.error(error); - toast({ - variant: "destructive", - title: t("targetWireGuardErrorInvalidIp"), - description: t("targetWireGuardErrorInvalidIpDescription") - }); - return; - } - } + // if (site && site.type == "wireguard" && site.subnet) { + // // make sure that the target IP is within the site subnet + // const targetIp = data.ip; + // const subnet = site.subnet; + // try { + // if (!isIPInSubnet(targetIp, subnet)) { + // toast({ + // variant: "destructive", + // title: t("targetWireGuardErrorInvalidIp"), + // description: t( + // "targetWireGuardErrorInvalidIpDescription" + // ) + // }); + // return; + // } + // } catch (error) { + // console.error(error); + // toast({ + // variant: "destructive", + // title: t("targetWireGuardErrorInvalidIp"), + // description: t("targetWireGuardErrorInvalidIpDescription") + // }); + // return; + // } + // } + + const site = sites.find((site) => site.siteId === data.siteId); const newTarget: LocalTarget = { ...data, + siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), new: true, @@ -311,10 +412,16 @@ export default function ReverseProxyTargets(props: { }; async function updateTarget(targetId: number, data: Partial) { + const site = sites.find((site) => site.siteId === data.siteId); setTargets( targets.map((target) => target.targetId === targetId - ? { ...target, ...data, updated: true } + ? { + ...target, + ...data, + updated: true, + siteType: site?.type || null + } : target ) ); @@ -332,7 +439,8 @@ export default function ReverseProxyTargets(props: { ip: target.ip, port: target.port, method: target.method, - enabled: target.enabled + enabled: target.enabled, + siteId: target.siteId }; if (target.new) { @@ -403,6 +511,135 @@ export default function ReverseProxyTargets(props: { } const columns: ColumnDef[] = [ + { + accessorKey: "siteId", + header: t("site"), + cell: ({ row }) => { + const selectedSite = sites.find( + (site) => site.siteId === row.original.siteId + ); + + const handleContainerSelectForTarget = ( + hostname: string, + port?: number + ) => { + updateTarget(row.original.targetId, { + ...row.original, + ip: hostname + }); + if (port) { + updateTarget(row.original.targetId, { + ...row.original, + port: port + }); + } + }; + + return ( +
+ + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + { + updateTarget( + row.original + .targetId, + { + siteId: site.siteId + } + ); + }} + > + + {site.name} + + ))} + + + + + + {selectedSite && selectedSite.type === "newt" && (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })()} +
+ ); + } + }, + ...(resource.http + ? [ + { + accessorKey: "method", + header: t("method"), + cell: ({ row }: { row: Row }) => ( + + ) + } + ] + : []), { accessorKey: "ip", header: t("targetAddr"), @@ -412,6 +649,7 @@ export default function ReverseProxyTargets(props: { className="min-w-[150px]" onBlur={(e) => updateTarget(row.original.targetId, { + ...row.original, ip: e.target.value }) } @@ -428,6 +666,7 @@ export default function ReverseProxyTargets(props: { className="min-w-[100px]" onBlur={(e) => updateTarget(row.original.targetId, { + ...row.original, port: parseInt(e.target.value, 10) }) } @@ -451,7 +690,7 @@ export default function ReverseProxyTargets(props: { // // // ), - // }, + // }, { accessorKey: "enabled", header: t("enabled"), @@ -459,7 +698,10 @@ export default function ReverseProxyTargets(props: { - updateTarget(row.original.targetId, { enabled: val }) + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) } /> ) @@ -489,33 +731,6 @@ export default function ReverseProxyTargets(props: { } ]; - if (resource.http) { - const methodCol: ColumnDef = { - accessorKey: "method", - header: t("method"), - cell: ({ row }) => ( - - ) - }; - - // add this to the first column - columns.unshift(methodCol); - } - const table = useReactTable({ data: targets, columns, @@ -545,221 +760,355 @@ export default function ReverseProxyTargets(props: { - -
+
+ - {targets.length >= 2 && ( +
( - - - { - field.onChange(val); - }} - /> - + + + {t("site")} + +
+ + + + + + + + + + + + {t( + "siteNotFound" + )} + + + {sites.map( + ( + site + ) => ( + { + addTargetForm.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + {field.value && + (() => { + const selectedSite = + sites.find( + (site) => + site.siteId === + field.value + ); + return selectedSite && + selectedSite.type === + "newt" ? (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })() : null; + })()} +
+
)} /> - )} - - - -
- -
- {resource.http && ( + {resource.http && ( + ( + + + {t("method")} + + + + + + + )} + /> + )} + ( - + - {t("method")} + {t("targetAddr")} - + )} /> - )} - - ( - - - {t("targetAddr")} - - - - - {site && site.type == "newt" && ( - { - addTargetForm.setValue( - "ip", - hostname - ); - if (port) { - addTargetForm.setValue( - "port", - port - ); - } - }} - /> - )} - - - )} - /> - ( - - - {t("targetPort")} - - - - - - - )} - /> - -
-
- - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - ( + + + {t("targetPort")} + + + + + + + )} + /> +
+ {t("targetSubmit")} + +
+ + +
+ + {targets.length > 0 ? ( + <> +
+ {t("targetsList")} +
+ +
+ + ( + + + { + field.onChange( + val + ); + }} + /> + + + )} + /> + + +
+
+ + + {table + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} + + + {table.getRowModel().rows?.length ? ( + table + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {t("targetNoOne")} + + + )} + + {/* */} + {/* {t('targetNoOneDescription')} */} + {/* */} +
+
+ + ) : ( +
+

+ {t("targetNoOne")} +

+
+ )}
@@ -885,7 +1234,7 @@ export default function ReverseProxyTargets(props: { proxySettingsLoading } > - {t("saveAllSettings")} + {t("saveSettings")}
diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index a8d926fe..438b8917 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -42,9 +42,7 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { subdomainSchema } from "@server/lib/schemas"; import { ListDomainsResponse } from "@server/routers/domain"; -import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; import { Command, CommandEmpty, @@ -66,10 +64,33 @@ import Link from "next/link"; import { useTranslations } from "next-intl"; import DomainPicker from "@app/components/DomainPicker"; import { build } from "@server/build"; +import { ContainersSelector } from "@app/components/ContainersSelector"; +import { + ColumnDef, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + getCoreRowModel, + useReactTable, + flexRender, + Row +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { Switch } from "@app/components/ui/switch"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { isTargetValid } from "@server/lib/validators"; +import { ListTargetsResponse } from "@server/routers/target"; +import { DockerManager, DockerState } from "@app/lib/docker"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), - siteId: z.number(), http: z.boolean() }); @@ -80,8 +101,15 @@ const httpResourceFormSchema = z.object({ const tcpUdpResourceFormSchema = z.object({ protocol: z.string(), - proxyPort: z.number().int().min(1).max(65535), - enableProxy: z.boolean().default(false) + proxyPort: z.number().int().min(1).max(65535) + // enableProxy: z.boolean().default(false) +}); + +const addTargetSchema = z.object({ + ip: z.string().refine(isTargetValid), + method: z.string().nullable(), + port: z.coerce.number().int().positive(), + siteId: z.number().int().positive() }); type BaseResourceFormValues = z.infer; @@ -97,6 +125,15 @@ interface ResourceTypeOption { disabled?: boolean; } +type LocalTarget = Omit< + ArrayElement & { + new?: boolean; + updated?: boolean; + siteType: string | null; + }, + "protocol" +>; + export default function Page() { const { env } = useEnvContext(); const api = createApiClient({ env }); @@ -113,6 +150,11 @@ export default function Page() { const [showSnippets, setShowSnippets] = useState(false); const [resourceId, setResourceId] = useState(null); + // Target management state + const [targets, setTargets] = useState([]); + const [targetsToRemove, setTargetsToRemove] = useState([]); + const [dockerStates, setDockerStates] = useState>(new Map()); + const resourceTypes: ReadonlyArray = [ { id: "http", @@ -147,11 +189,128 @@ export default function Page() { resolver: zodResolver(tcpUdpResourceFormSchema), defaultValues: { protocol: "tcp", - proxyPort: undefined, - enableProxy: false + proxyPort: undefined + // enableProxy: false } }); + const addTargetForm = useForm({ + resolver: zodResolver(addTargetSchema), + defaultValues: { + ip: "", + method: baseForm.watch("http") ? "http" : null, + port: "" as any as number + } as z.infer + }); + + const watchedIp = addTargetForm.watch("ip"); + const watchedPort = addTargetForm.watch("port"); + const watchedSiteId = addTargetForm.watch("siteId"); + + const handleContainerSelect = (hostname: string, port?: number) => { + addTargetForm.setValue("ip", hostname); + if (port) { + addTargetForm.setValue("port", port); + } + }; + + const initializeDockerForSite = async (siteId: number) => { + if (dockerStates.has(siteId)) { + return; // Already initialized + } + + const dockerManager = new DockerManager(api, siteId); + const dockerState = await dockerManager.initializeDocker(); + + setDockerStates(prev => new Map(prev.set(siteId, dockerState))); + }; + + const refreshContainersForSite = async (siteId: number) => { + const dockerManager = new DockerManager(api, siteId); + const containers = await dockerManager.fetchContainers(); + + setDockerStates(prev => { + const newMap = new Map(prev); + const existingState = newMap.get(siteId); + if (existingState) { + newMap.set(siteId, { ...existingState, containers }); + } + return newMap; + }); + }; + + const getDockerStateForSite = (siteId: number): DockerState => { + return dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + }; + }; + + async function addTarget(data: z.infer) { + // Check if target with same IP, port and method already exists + const isDuplicate = targets.some( + (target) => + target.ip === data.ip && + target.port === data.port && + target.method === data.method && + target.siteId === data.siteId + ); + + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("targetErrorDuplicate"), + description: t("targetErrorDuplicateDescription") + }); + return; + } + + const site = sites.find((site) => site.siteId === data.siteId); + + const newTarget: LocalTarget = { + ...data, + siteType: site?.type || null, + enabled: true, + targetId: new Date().getTime(), + new: true, + resourceId: 0 // Will be set when resource is created + }; + + setTargets([...targets, newTarget]); + addTargetForm.reset({ + ip: "", + method: baseForm.watch("http") ? "http" : null, + port: "" as any as number + }); + } + + const removeTarget = (targetId: number) => { + setTargets([ + ...targets.filter((target) => target.targetId !== targetId) + ]); + + if (!targets.find((target) => target.targetId === targetId)?.new) { + setTargetsToRemove([...targetsToRemove, targetId]); + } + }; + + async function updateTarget(targetId: number, data: Partial) { + const site = sites.find((site) => site.siteId === data.siteId); + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...data, + updated: true, + siteType: site?.type || null + } + : target + ) + ); + } + async function onSubmit() { setCreateLoading(true); @@ -161,7 +320,6 @@ export default function Page() { try { const payload = { name: baseData.name, - siteId: baseData.siteId, http: baseData.http }; @@ -176,15 +334,15 @@ export default function Page() { const tcpUdpData = tcpUdpForm.getValues(); Object.assign(payload, { protocol: tcpUdpData.protocol, - proxyPort: tcpUdpData.proxyPort, - enableProxy: tcpUdpData.enableProxy + proxyPort: tcpUdpData.proxyPort + // enableProxy: tcpUdpData.enableProxy }); } const res = await api .put< AxiosResponse - >(`/org/${orgId}/site/${baseData.siteId}/resource/`, payload) + >(`/org/${orgId}/resource/`, payload) .catch((e) => { toast({ variant: "destructive", @@ -200,18 +358,45 @@ export default function Page() { const id = res.data.data.resourceId; setResourceId(id); + // Create targets if any exist + if (targets.length > 0) { + try { + for (const target of targets) { + const data = { + ip: target.ip, + port: target.port, + method: target.method, + enabled: target.enabled, + siteId: target.siteId + }; + + await api.put(`/resource/${id}/target`, data); + } + } catch (targetError) { + console.error("Error creating targets:", targetError); + toast({ + variant: "destructive", + title: t("targetErrorCreate"), + description: formatAxiosError( + targetError, + t("targetErrorCreateDescription") + ) + }); + } + } + if (isHttp) { router.push(`/${orgId}/settings/resources/${id}`); } else { const tcpUdpData = tcpUdpForm.getValues(); // Only show config snippets if enableProxy is explicitly true - if (tcpUdpData.enableProxy === true) { - setShowSnippets(true); - router.refresh(); - } else { - // If enableProxy is false or undefined, go directly to resource page - router.push(`/${orgId}/settings/resources/${id}`); - } + // if (tcpUdpData.enableProxy === true) { + setShowSnippets(true); + router.refresh(); + // } else { + // // If enableProxy is false or undefined, go directly to resource page + // router.push(`/${orgId}/settings/resources/${id}`); + // } } } } catch (e) { @@ -249,8 +434,16 @@ export default function Page() { if (res?.status === 200) { setSites(res.data.data.sites); - if (res.data.data.sites.length > 0) { - baseForm.setValue( + // Initialize Docker for newt sites + for (const site of res.data.data.sites) { + if (site.type === "newt") { + initializeDockerForSite(site.siteId); + } + } + + // If there's only one site, set it as the default in the form + if (res.data.data.sites.length) { + addTargetForm.setValue( "siteId", res.data.data.sites[0].siteId ); @@ -292,6 +485,216 @@ export default function Page() { load(); }, []); + const columns: ColumnDef[] = [ + { + accessorKey: "siteId", + header: t("site"), + cell: ({ row }) => { + const selectedSite = sites.find( + (site) => site.siteId === row.original.siteId + ); + + const handleContainerSelectForTarget = ( + hostname: string, + port?: number + ) => { + updateTarget(row.original.targetId, { + ...row.original, + ip: hostname + }); + if (port) { + updateTarget(row.original.targetId, { + ...row.original, + port: port + }); + } + }; + + return ( +
+ + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + { + updateTarget( + row.original + .targetId, + { + siteId: site.siteId + } + ); + }} + > + + {site.name} + + ))} + + + + + + {selectedSite && selectedSite.type === "newt" && (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })()} +
+ ); + } + }, + ...(baseForm.watch("http") + ? [ + { + accessorKey: "method", + header: t("method"), + cell: ({ row }: { row: Row }) => ( + + ) + } + ] + : []), + { + accessorKey: "ip", + header: t("targetAddr"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + ip: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "port", + header: t("targetPort"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + port: parseInt(e.target.value, 10) + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: t("enabled"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> + ) + }, + { + id: "actions", + cell: ({ row }) => ( + <> +
+ +
+ + ) + } + ]; + + const table = useReactTable({ + data: targets, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + pagination: { + pageIndex: 0, + pageSize: 1000 + } + } + }); + return ( <>
@@ -348,104 +751,6 @@ export default function Page() { )} /> - - ( - - - {t("site")} - - - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - baseForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - {t( - "siteSelectionDescription" - )} - - - )} - /> @@ -471,6 +776,13 @@ export default function Page() { "http", value === "http" ); + // Update method default when switching resource type + addTargetForm.setValue( + "method", + value === "http" + ? "http" + : null + ); }} cols={2} /> @@ -616,7 +928,7 @@ export default function Page() { )} /> - {build == "oss" && ( + {/* {build == "oss" && ( )} /> - )} + )} */} @@ -662,6 +974,379 @@ export default function Page() { )} + + + + {t("targets")} + + + {t("targetsDescription")} + + + +
+
+ +
+ ( + + + {t("site")} + +
+ + + + + + + + + + + + {t( + "siteNotFound" + )} + + + {sites.map( + ( + site + ) => ( + { + addTargetForm.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + {field.value && + (() => { + const selectedSite = + sites.find( + ( + site + ) => + site.siteId === + field.value + ); + return selectedSite && + selectedSite.type === + "newt" ? (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })() : null; + })()} +
+ +
+ )} + /> + + {baseForm.watch("http") && ( + ( + + + {t( + "method" + )} + + + + + + + )} + /> + )} + + ( + + + {t( + "targetAddr" + )} + + + + + + + )} + /> + ( + + + {t( + "targetPort" + )} + + + + + + + )} + /> + +
+
+ +
+ + {targets.length > 0 ? ( + <> +
+ {t("targetsList")} +
+
+ + + {table + .getHeaderGroups() + .map( + ( + headerGroup + ) => ( + + {headerGroup.headers.map( + ( + header + ) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ) + )} + + + {table.getRowModel() + .rows?.length ? ( + table + .getRowModel() + .rows.map( + (row) => ( + + {row + .getVisibleCells() + .map( + ( + cell + ) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ) + )} + + ) + ) + ) : ( + + + {t( + "targetNoOne" + )} + + + )} + +
+
+ + ) : ( +
+

+ {t("targetNoOne")} +

+
+ )} +
+
+
diff --git a/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx index 6094f167..36ab1727 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx @@ -33,9 +33,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { return ( - - {t("siteInfo")} - + {(site.type == "newt" || site.type == "wireguard") && ( <> diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index f92a5090..8bd8dc4b 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -38,12 +38,14 @@ import { Tag, TagInput } from "@app/components/tags/tag-input"; const GeneralFormSchema = z.object({ name: z.string().nonempty("Name is required"), dockerSocketEnabled: z.boolean().optional(), - remoteSubnets: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ).optional() + remoteSubnets: z + .array( + z.object({ + id: z.string(), + text: z.string() + }) + ) + .optional() }); type GeneralFormValues = z.infer; @@ -55,7 +57,9 @@ export default function GeneralPage() { const api = createApiClient(useEnvContext()); const [loading, setLoading] = useState(false); - const [activeCidrTagIndex, setActiveCidrTagIndex] = useState(null); + const [activeCidrTagIndex, setActiveCidrTagIndex] = useState( + null + ); const router = useRouter(); const t = useTranslations(); @@ -66,10 +70,10 @@ export default function GeneralPage() { name: site?.name, dockerSocketEnabled: site?.dockerSocketEnabled ?? false, remoteSubnets: site?.remoteSubnets - ? site.remoteSubnets.split(',').map((subnet, index) => ({ - id: subnet.trim(), - text: subnet.trim() - })) + ? site.remoteSubnets.split(",").map((subnet, index) => ({ + id: subnet.trim(), + text: subnet.trim() + })) : [] }, mode: "onChange" @@ -82,7 +86,10 @@ export default function GeneralPage() { .post(`/site/${site?.siteId}`, { name: data.name, dockerSocketEnabled: data.dockerSocketEnabled, - remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || '' + remoteSubnets: + data.remoteSubnets + ?.map((subnet) => subnet.text) + .join(",") || "" }) .catch((e) => { toast({ @@ -98,7 +105,8 @@ export default function GeneralPage() { updateSite({ name: data.name, dockerSocketEnabled: data.dockerSocketEnabled, - remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || '' + remoteSubnets: + data.remoteSubnets?.map((subnet) => subnet.text).join(",") || "" }); toast({ @@ -145,42 +153,64 @@ export default function GeneralPage() { )} /> - ( - - {t("remoteSubnets")} - - { - form.setValue( - "remoteSubnets", - newSubnets as Tag[] - ); - }} - validateTag={(tag) => { - // Basic CIDR validation regex - const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/; - return cidrRegex.test(tag); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("remoteSubnetsDescription")} - - - - )} - /> + {env.flags.enableClients && + site.type === "newt" ? ( + ( + + + {t("remoteSubnets")} + + + { + form.setValue( + "remoteSubnets", + newSubnets as Tag[] + ); + }} + validateTag={(tag) => { + // Basic CIDR validation regex + const cidrRegex = + /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/; + return cidrRegex.test( + tag + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t( + "remoteSubnetsDescription" + )} + + + + )} + /> + ) : null} {site && site.type === "newt" && ( {t("siteConfiguration")}

-
+
{t("setupToken")} - + + + {t("setupTokenDescription")} + )} diff --git a/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx b/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx new file mode 100644 index 00000000..c489a759 --- /dev/null +++ b/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { GenerateOidcUrlResponse } from "@server/routers/idp"; +import { AxiosResponse } from "axios"; +import { useRouter } from "next/navigation"; +import { + Card, + CardHeader, + CardTitle, + CardContent, + CardDescription +} from "@app/components/ui/card"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; +import { useTranslations } from "next-intl"; + +type AutoLoginHandlerProps = { + resourceId: number; + skipToIdpId: number; + redirectUrl: string; +}; + +export default function AutoLoginHandler({ + resourceId, + skipToIdpId, + redirectUrl +}: AutoLoginHandlerProps) { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const router = useRouter(); + const t = useTranslations(); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function initiateAutoLogin() { + setLoading(true); + + try { + const res = await api.post< + AxiosResponse + >(`/auth/idp/${skipToIdpId}/oidc/generate-url`, { + redirectUrl + }); + + if (res.data.data.redirectUrl) { + // Redirect to the IDP for authentication + window.location.href = res.data.data.redirectUrl; + } else { + setError(t("autoLoginErrorNoRedirectUrl")); + } + } catch (e) { + console.error("Failed to generate OIDC URL:", e); + setError(formatAxiosError(e, t("autoLoginErrorGeneratingUrl"))); + } finally { + setLoading(false); + } + } + + initiateAutoLogin(); + }, []); + + return ( +
+ + + {t("autoLoginTitle")} + {t("autoLoginDescription")} + + + {loading && ( +
+ + {t("autoLoginProcessing")} +
+ )} + {!loading && !error && ( +
+ + {t("autoLoginRedirecting")} +
+ )} + {error && ( + + + + {t("autoLoginError")} + {error} + + + )} +
+
+
+ ); +} diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 9032ae18..347d3586 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -15,6 +15,7 @@ import AccessToken from "./AccessToken"; import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; import { ListIdpsResponse } from "@server/routers/idp"; +import AutoLoginHandler from "./AutoLoginHandler"; export const dynamic = "force-dynamic"; @@ -30,11 +31,13 @@ export default async function ResourceAuthPage(props: { const env = pullEnv(); + const authHeader = await authCookieHeader(); + let authInfo: GetResourceAuthInfoResponse | undefined; try { const res = await internal.get< AxiosResponse - >(`/resource/${params.resourceId}/auth`, await authCookieHeader()); + >(`/resource/${params.resourceId}/auth`, authHeader); if (res && res.status === 200) { authInfo = res.data.data; @@ -62,10 +65,9 @@ export default async function ResourceAuthPage(props: { const redirectPort = new URL(searchParams.redirect).port; const serverResourceHostWithPort = `${serverResourceHost}:${redirectPort}`; - if (serverResourceHost === redirectHost) { redirectUrl = searchParams.redirect; - } else if ( serverResourceHostWithPort === redirectHost ) { + } else if (serverResourceHostWithPort === redirectHost) { redirectUrl = searchParams.redirect; } } catch (e) {} @@ -144,6 +146,19 @@ export default async function ResourceAuthPage(props: { name: idp.name })) as LoginFormIDP[]; + if (authInfo.skipToIdpId && authInfo.skipToIdpId !== null) { + const idp = loginIdps.find((idp) => idp.idpId === authInfo.skipToIdpId); + if (idp) { + return ( + + ); + } + } + return ( <> {userIsUnauthorized && isSSOOnly ? ( diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx index 0f09fb5a..7ed31b62 100644 --- a/src/components/ContainersSelector.tsx +++ b/src/components/ContainersSelector.tsx @@ -43,35 +43,30 @@ import { } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Search, RefreshCw, Filter, Columns } from "lucide-react"; -import { GetSiteResponse, Container } from "@server/routers/site"; -import { useDockerSocket } from "@app/hooks/useDockerSocket"; +import { Container } from "@server/routers/site"; import { useTranslations } from "next-intl"; - -// Type definitions based on the JSON structure +import { FaDocker } from "react-icons/fa"; interface ContainerSelectorProps { - site: GetSiteResponse; + site: { siteId: number; name: string; type: string }; + containers: Container[]; + isAvailable: boolean; onContainerSelect?: (hostname: string, port?: number) => void; + onRefresh?: () => void; } export const ContainersSelector: FC = ({ site, - onContainerSelect + containers, + isAvailable, + onContainerSelect, + onRefresh }) => { const [open, setOpen] = useState(false); const t = useTranslations(); - const { isAvailable, containers, fetchContainers } = useDockerSocket(site); - - useEffect(() => { - console.log("DockerSocket isAvailable:", isAvailable); - if (isAvailable) { - fetchContainers(); - } - }, [isAvailable]); - - if (!site || !isAvailable) { + if (!site || !isAvailable || site.type !== "newt") { return null; } @@ -84,13 +79,14 @@ export const ContainersSelector: FC = ({ return ( <> -
setOpen(true)} + title={t("viewDockerContainers")} > - {t("viewDockerContainers")} - + + @@ -106,7 +102,7 @@ export const ContainersSelector: FC = ({ fetchContainers()} + onRefresh={onRefresh || (() => {})} />
@@ -263,7 +259,9 @@ const DockerContainersTable: FC<{ size="sm" className="h-6 px-2 text-xs hover:bg-muted" > - {t("containerLabelsCount", { count: labelEntries.length })} + {t("containerLabelsCount", { + count: labelEntries.length + })} @@ -279,7 +277,10 @@ const DockerContainersTable: FC<{ {key}
- {value || t("containerLabelEmpty")} + {value || + t( + "containerLabelEmpty" + )}
))} @@ -316,7 +317,9 @@ const DockerContainersTable: FC<{ onContainerSelect(row.original, ports[0])} + onClick={() => + onContainerSelect(row.original, ports[0]) + } disabled={row.original.state !== "running"} > {t("select")} @@ -415,9 +420,7 @@ const DockerContainersTable: FC<{ hideStoppedContainers) && containers.length > 0 ? ( <> -

- {t("noContainersMatchingFilters")} -

+

{t("noContainersMatchingFilters")}

{hideContainersWithoutPorts && (
@@ -463,7 +464,9 @@ const DockerContainersTable: FC<{
setSearchInput(event.target.value) @@ -473,7 +476,10 @@ const DockerContainersTable: FC<{ {searchInput && table.getFilteredRowModel().rows.length > 0 && (
- {t("searchResultsCount", { count: table.getFilteredRowModel().rows.length })} + {t("searchResultsCount", { + count: table.getFilteredRowModel().rows + .length + })}
)}
@@ -644,7 +650,9 @@ const DockerContainersTable: FC<{ {t("searching")}
) : ( - t("noContainersFoundMatching", { filter: globalFilter }) + t("noContainersFoundMatching", { + filter: globalFilter + }) )} diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx new file mode 100644 index 00000000..3c4841d7 --- /dev/null +++ b/src/components/CreateInternalResourceDialog.tsx @@ -0,0 +1,422 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { toast } from "@app/hooks/useToast"; +import { useTranslations } from "next-intl"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { ListSitesResponse } from "@server/routers/site"; +import { cn } from "@app/lib/cn"; + +type Site = ListSitesResponse["sites"][0]; + +type CreateInternalResourceDialogProps = { + open: boolean; + setOpen: (val: boolean) => void; + orgId: string; + sites: Site[]; + onSuccess?: () => void; +}; + +export default function CreateInternalResourceDialog({ + open, + setOpen, + orgId, + sites, + onSuccess +}: CreateInternalResourceDialogProps) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const [isSubmitting, setIsSubmitting] = useState(false); + + const formSchema = z.object({ + name: z + .string() + .min(1, t("createInternalResourceDialogNameRequired")) + .max(255, t("createInternalResourceDialogNameMaxLength")), + siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")), + protocol: z.enum(["tcp", "udp"]), + proxyPort: z + .number() + .int() + .positive() + .min(1, t("createInternalResourceDialogProxyPortMin")) + .max(65535, t("createInternalResourceDialogProxyPortMax")), + destinationIp: z.string().ip(t("createInternalResourceDialogInvalidIPAddressFormat")), + destinationPort: z + .number() + .int() + .positive() + .min(1, t("createInternalResourceDialogDestinationPortMin")) + .max(65535, t("createInternalResourceDialogDestinationPortMax")) + }); + + type FormData = z.infer; + + const availableSites = sites.filter( + (site) => site.type === "newt" && site.subnet + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + siteId: availableSites[0]?.siteId || 0, + protocol: "tcp", + proxyPort: undefined, + destinationIp: "", + destinationPort: undefined + } + }); + + useEffect(() => { + if (open && availableSites.length > 0) { + form.reset({ + name: "", + siteId: availableSites[0].siteId, + protocol: "tcp", + proxyPort: undefined, + destinationIp: "", + destinationPort: undefined + }); + } + }, [open]); + + const handleSubmit = async (data: FormData) => { + setIsSubmitting(true); + try { + await api.put(`/org/${orgId}/site/${data.siteId}/resource`, { + name: data.name, + protocol: data.protocol, + proxyPort: data.proxyPort, + destinationIp: data.destinationIp, + destinationPort: data.destinationPort, + enabled: true + }); + + toast({ + title: t("createInternalResourceDialogSuccess"), + description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"), + variant: "default" + }); + + onSuccess?.(); + setOpen(false); + } catch (error) { + console.error("Error creating internal resource:", error); + toast({ + title: t("createInternalResourceDialogError"), + description: formatAxiosError( + error, + t("createInternalResourceDialogFailedToCreateInternalResource") + ), + variant: "destructive" + }); + } finally { + setIsSubmitting(false); + } + }; + + if (availableSites.length === 0) { + return ( + + + + {t("createInternalResourceDialogNoSitesAvailable")} + + {t("createInternalResourceDialogNoSitesAvailableDescription")} + + + + + + + + ); + } + + return ( + + + + {t("createInternalResourceDialogCreateClientResource")} + + {t("createInternalResourceDialogCreateClientResourceDescription")} + + + +
+ + {/* Resource Properties Form */} +
+

+ {t("createInternalResourceDialogResourceProperties")} +

+
+ ( + + {t("createInternalResourceDialogName")} + + + + + + )} + /> + +
+ ( + + {t("createInternalResourceDialogSite")} + + + + + + + + + + + {t("createInternalResourceDialogNoSitesFound")} + + {availableSites.map((site) => ( + { + field.onChange(site.siteId); + }} + > + + {site.name} + + ))} + + + + + + + + )} + /> + + ( + + + {t("createInternalResourceDialogProtocol")} + + + + + )} + /> +
+ + ( + + {t("createInternalResourceDialogSitePort")} + + + field.onChange( + e.target.value === "" ? undefined : parseInt(e.target.value) + ) + } + /> + + + {t("createInternalResourceDialogSitePortDescription")} + + + + )} + /> +
+
+ + {/* Target Configuration Form */} +
+

+ {t("createInternalResourceDialogTargetConfiguration")} +

+
+
+ ( + + + {t("createInternalResourceDialogDestinationIP")} + + + + + + {t("createInternalResourceDialogDestinationIPDescription")} + + + + )} + /> + + ( + + + {t("createInternalResourceDialogDestinationPort")} + + + + field.onChange( + e.target.value === "" ? undefined : parseInt(e.target.value) + ) + } + /> + + + {t("createInternalResourceDialogDestinationPortDescription")} + + + + )} + /> +
+
+
+
+ +
+ + + + +
+
+ ); +} diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 5f4104ea..1fc856c9 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -3,13 +3,28 @@ import { useState, useEffect, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@/components/ui/popover"; import { AlertCircle, CheckCircle2, Building2, Zap, + Check, + ChevronsUpDown, ArrowUpDown } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -19,9 +34,9 @@ import { toast } from "@/hooks/useToast"; import { ListDomainsResponse } from "@server/routers/domain/listDomains"; import { AxiosResponse } from "axios"; import { cn } from "@/lib/cn"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; type OrganizationDomain = { domainId: string; @@ -39,17 +54,15 @@ type AvailableOption = { type DomainOption = { id: string; domain: string; - type: "organization" | "provided"; + type: "organization" | "provided" | "provided-search"; verified?: boolean; domainType?: "ns" | "cname" | "wildcard"; domainId?: string; domainNamespaceId?: string; - subdomain?: string; }; -interface DomainPickerProps { +interface DomainPicker2Props { orgId: string; - cols?: number; onDomainChange?: (domainInfo: { domainId: string; domainNamespaceId?: string; @@ -58,34 +71,37 @@ interface DomainPickerProps { fullDomain: string; baseDomain: string; }) => void; + cols?: number; } -export default function DomainPicker({ +export default function DomainPicker2({ orgId, - cols, - onDomainChange -}: DomainPickerProps) { + onDomainChange, + cols = 2 +}: DomainPicker2Props) { const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); - const [userInput, setUserInput] = useState(""); - const [selectedOption, setSelectedOption] = useState( - null - ); + const [subdomainInput, setSubdomainInput] = useState(""); + const [selectedBaseDomain, setSelectedBaseDomain] = + useState(null); const [availableOptions, setAvailableOptions] = useState( [] ); - const [isChecking, setIsChecking] = useState(false); const [organizationDomains, setOrganizationDomains] = useState< OrganizationDomain[] >([]); const [loadingDomains, setLoadingDomains] = useState(false); + const [open, setOpen] = useState(false); + + // Provided domain search states + const [userInput, setUserInput] = useState(""); + const [isChecking, setIsChecking] = useState(false); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); - const [activeTab, setActiveTab] = useState< - "all" | "organization" | "provided" - >("all"); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); + const [selectedProvidedDomain, setSelectedProvidedDomain] = + useState(null); useEffect(() => { const loadOrganizationDomains = async () => { @@ -107,6 +123,41 @@ export default function DomainPicker({ type: domain.type as "ns" | "cname" | "wildcard" })); setOrganizationDomains(domains); + + // Auto-select first available domain + if (domains.length > 0) { + // Select the first organization domain + const firstOrgDomain = domains[0]; + const domainOption: DomainOption = { + id: `org-${firstOrgDomain.domainId}`, + domain: firstOrgDomain.baseDomain, + type: "organization", + verified: firstOrgDomain.verified, + domainType: firstOrgDomain.type, + domainId: firstOrgDomain.domainId + }; + setSelectedBaseDomain(domainOption); + + onDomainChange?.({ + domainId: firstOrgDomain.domainId, + type: "organization", + subdomain: undefined, + fullDomain: firstOrgDomain.baseDomain, + baseDomain: firstOrgDomain.baseDomain + }); + } else if (build === "saas" || build === "enterprise") { + // If no organization domains, select the provided domain option + const domainOptionText = + build === "enterprise" + ? "Provided Domain" + : "Free Provided Domain"; + const freeDomainOption: DomainOption = { + id: "provided-search", + domain: domainOptionText, + type: "provided-search" + }; + setSelectedBaseDomain(freeDomainOption); + } } } catch (error) { console.error("Failed to load organization domains:", error); @@ -123,135 +174,131 @@ export default function DomainPicker({ loadOrganizationDomains(); }, [orgId, api]); - // Generate domain options based on user input - const generateDomainOptions = (): DomainOption[] => { + const checkAvailability = useCallback( + async (input: string) => { + if (!input.trim()) { + setAvailableOptions([]); + setIsChecking(false); + return; + } + + setIsChecking(true); + try { + const checkSubdomain = input + .toLowerCase() + .replace(/\./g, "-") + .replace(/[^a-z0-9-]/g, "") + .replace(/-+/g, "-"); + } catch (error) { + console.error("Failed to check domain availability:", error); + setAvailableOptions([]); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to check domain availability" + }); + } finally { + setIsChecking(false); + } + }, + [api] + ); + + const debouncedCheckAvailability = useCallback( + debounce(checkAvailability, 500), + [checkAvailability] + ); + + useEffect(() => { + if (selectedBaseDomain?.type === "provided-search") { + setProvidedDomainsShown(3); + setSelectedProvidedDomain(null); + + if (userInput.trim()) { + setIsChecking(true); + debouncedCheckAvailability(userInput); + } else { + setAvailableOptions([]); + setIsChecking(false); + } + } + }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); + + const generateDropdownOptions = (): DomainOption[] => { const options: DomainOption[] = []; - if (!userInput.trim()) return options; - - // Add organization domain options organizationDomains.forEach((orgDomain) => { - if (orgDomain.type === "cname") { - // For CNAME domains, check if the user input matches exactly - if ( - orgDomain.baseDomain.toLowerCase() === - userInput.toLowerCase() - ) { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: "cname", - domainId: orgDomain.domainId - }); - } - } else if (orgDomain.type === "ns") { - // For NS domains, check if the user input could be a subdomain - const userInputLower = userInput.toLowerCase(); - const baseDomainLower = orgDomain.baseDomain.toLowerCase(); - - // Check if user input ends with the base domain - if (userInputLower.endsWith(`.${baseDomainLower}`)) { - const subdomain = userInputLower.slice( - 0, - -(baseDomainLower.length + 1) - ); - options.push({ - id: `org-${orgDomain.domainId}`, - domain: userInput, - type: "organization", - verified: orgDomain.verified, - domainType: "ns", - domainId: orgDomain.domainId, - subdomain: subdomain - }); - } else if (userInputLower === baseDomainLower) { - // Exact match for base domain - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: "ns", - domainId: orgDomain.domainId - }); - } - } else if (orgDomain.type === "wildcard") { - // For wildcard domains, allow the base domain or multiple levels up - const userInputLower = userInput.toLowerCase(); - const baseDomainLower = orgDomain.baseDomain.toLowerCase(); - - // Check if user input is exactly the base domain - if (userInputLower === baseDomainLower) { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: "wildcard", - domainId: orgDomain.domainId - }); - } - // Check if user input ends with the base domain (allows multiple level subdomains) - else if (userInputLower.endsWith(`.${baseDomainLower}`)) { - const subdomain = userInputLower.slice( - 0, - -(baseDomainLower.length + 1) - ); - // Allow multiple levels (subdomain can contain dots) - options.push({ - id: `org-${orgDomain.domainId}`, - domain: userInput, - type: "organization", - verified: orgDomain.verified, - domainType: "wildcard", - domainId: orgDomain.domainId, - subdomain: subdomain - }); - } - } - }); - - // Add provided domain options (always try to match provided domains) - availableOptions.forEach((option) => { options.push({ - id: `provided-${option.domainNamespaceId}`, - domain: option.fullDomain, - type: "provided", - domainNamespaceId: option.domainNamespaceId, - domainId: option.domainId + id: `org-${orgDomain.domainId}`, + domain: orgDomain.baseDomain, + type: "organization", + verified: orgDomain.verified, + domainType: orgDomain.type, + domainId: orgDomain.domainId }); }); - // Sort options - return options.sort((a, b) => { - const comparison = a.domain.localeCompare(b.domain); - return sortOrder === "asc" ? comparison : -comparison; - }); + if (build === "saas" || build === "enterprise") { + const domainOptionText = + build === "enterprise" + ? "Provided Domain" + : "Free Provided Domain"; + options.push({ + id: "provided-search", + domain: domainOptionText, + type: "provided-search" + }); + } + + return options; }; - const domainOptions = generateDomainOptions(); + const dropdownOptions = generateDropdownOptions(); - // Filter options based on active tab - const filteredOptions = domainOptions.filter((option) => { - if (activeTab === "all") return true; - return option.type === activeTab; - }); + const validateSubdomain = ( + subdomain: string, + baseDomain: DomainOption + ): boolean => { + if (!baseDomain) return false; - // Separate organization and provided options for pagination - const organizationOptions = filteredOptions.filter( - (opt) => opt.type === "organization" - ); - const allProvidedOptions = filteredOptions.filter( - (opt) => opt.type === "provided" - ); - const providedOptions = allProvidedOptions.slice(0, providedDomainsShown); - const hasMoreProvided = allProvidedOptions.length > providedDomainsShown; + if (baseDomain.type === "provided-search") { + return /^[a-zA-Z0-9-]+$/.test(subdomain); + } - // Handle option selection - const handleOptionSelect = (option: DomainOption) => { - setSelectedOption(option); + if (baseDomain.type === "organization") { + if (baseDomain.domainType === "cname") { + return subdomain === ""; + } else if (baseDomain.domainType === "ns") { + return /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/.test(subdomain); + } else if (baseDomain.domainType === "wildcard") { + return /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/.test(subdomain); + } + } + + return false; + }; + + // Handle base domain selection + const handleBaseDomainSelect = (option: DomainOption) => { + setSelectedBaseDomain(option); + setOpen(false); + + if (option.domainType === "cname") { + setSubdomainInput(""); + } + + if (option.type === "provided-search") { + setUserInput(""); + setAvailableOptions([]); + setSelectedProvidedDomain(null); + onDomainChange?.({ + domainId: option.domainId!, + type: "organization", + subdomain: undefined, + fullDomain: option.domain, + baseDomain: option.domain + }); + } if (option.type === "organization") { if (option.domainType === "cname") { @@ -262,258 +309,413 @@ export default function DomainPicker({ fullDomain: option.domain, baseDomain: option.domain }); - } else if (option.domainType === "ns") { - const subdomain = option.subdomain || ""; + } else { onDomainChange?.({ domainId: option.domainId!, type: "organization", - subdomain: subdomain || undefined, + subdomain: undefined, fullDomain: option.domain, baseDomain: option.domain }); - } else if (option.domainType === "wildcard") { + } + } + }; + + const handleSubdomainChange = (value: string) => { + const validInput = value.replace(/[^a-zA-Z0-9.-]/g, ""); + setSubdomainInput(validInput); + + setSelectedProvidedDomain(null); + + if (selectedBaseDomain && selectedBaseDomain.type === "organization") { + const isValid = validateSubdomain(validInput, selectedBaseDomain); + if (isValid) { + const fullDomain = validInput + ? `${validInput}.${selectedBaseDomain.domain}` + : selectedBaseDomain.domain; onDomainChange?.({ - domainId: option.domainId!, + domainId: selectedBaseDomain.domainId!, type: "organization", - subdomain: option.subdomain || undefined, - fullDomain: option.domain, - baseDomain: option.subdomain - ? option.domain.split(".").slice(1).join(".") - : option.domain + subdomain: validInput || undefined, + fullDomain: fullDomain, + baseDomain: selectedBaseDomain.domain + }); + } else if (validInput === "") { + onDomainChange?.({ + domainId: selectedBaseDomain.domainId!, + type: "organization", + subdomain: undefined, + fullDomain: selectedBaseDomain.domain, + baseDomain: selectedBaseDomain.domain }); } - } else if (option.type === "provided") { - // Extract subdomain from full domain - const parts = option.domain.split("."); - const subdomain = parts[0]; - const baseDomain = parts.slice(1).join("."); + } + }; + + const handleProvidedDomainInputChange = (value: string) => { + const validInput = value.replace(/[^a-zA-Z0-9.-]/g, ""); + setUserInput(validInput); + + // Clear selected domain when user types + if (selectedProvidedDomain) { + setSelectedProvidedDomain(null); onDomainChange?.({ - domainId: option.domainId!, - domainNamespaceId: option.domainNamespaceId, + domainId: "", type: "provided", - subdomain: subdomain, - fullDomain: option.domain, - baseDomain: baseDomain + subdomain: undefined, + fullDomain: "", + baseDomain: "" }); } }; + const handleProvidedDomainSelect = (option: AvailableOption) => { + setSelectedProvidedDomain(option); + + const parts = option.fullDomain.split("."); + const subdomain = parts[0]; + const baseDomain = parts.slice(1).join("."); + + onDomainChange?.({ + domainId: option.domainId, + domainNamespaceId: option.domainNamespaceId, + type: "provided", + subdomain: subdomain, + fullDomain: option.fullDomain, + baseDomain: baseDomain + }); + }; + + const isSubdomainValid = selectedBaseDomain + ? validateSubdomain(subdomainInput, selectedBaseDomain) + : true; + const showSubdomainInput = + selectedBaseDomain && + selectedBaseDomain.type === "organization" && + selectedBaseDomain.domainType !== "cname"; + const showProvidedDomainSearch = + selectedBaseDomain?.type === "provided-search"; + + const sortedAvailableOptions = availableOptions.sort((a, b) => { + const comparison = a.fullDomain.localeCompare(b.fullDomain); + return sortOrder === "asc" ? comparison : -comparison; + }); + + const displayedProvidedOptions = sortedAvailableOptions.slice( + 0, + providedDomainsShown + ); + const hasMoreProvided = + sortedAvailableOptions.length > providedDomainsShown; + return ( -
- {/* Domain Input */} -
- - { - // Only allow letters, numbers, hyphens, and periods - const validInput = e.target.value.replace( - /[^a-zA-Z0-9.-]/g, - "" - ); - setUserInput(validInput); - // Clear selection when input changes - setSelectedOption(null); - }} - /> -

- {build === "saas" - ? t("domainPickerDescriptionSaas") - : t("domainPickerDescription")} -

+
+
+
+ + { + if (showProvidedDomainSearch) { + handleProvidedDomainInputChange(e.target.value); + } else { + handleSubdomainChange(e.target.value); + } + }} + /> + {showSubdomainInput && !subdomainInput && ( +

+ {t("domainPickerEnterSubdomainOrLeaveBlank")} +

+ )} + {showProvidedDomainSearch && !userInput && ( +

+ {t("domainPickerEnterSubdomainToSearch")} +

+ )} +
+ +
+ + + + + + + + + +
+ {t("domainPickerNoDomainsFound")} +
+
+ + {organizationDomains.length > 0 && ( + <> + + + {organizationDomains.map( + (orgDomain) => ( + + handleBaseDomainSelect( + { + id: `org-${orgDomain.domainId}`, + domain: orgDomain.baseDomain, + type: "organization", + verified: + orgDomain.verified, + domainType: + orgDomain.type, + domainId: + orgDomain.domainId + } + ) + } + className="mx-2 rounded-md" + disabled={ + !orgDomain.verified + } + > +
+ +
+
+ + { + orgDomain.baseDomain + } + + + {orgDomain.type.toUpperCase()}{" "} + •{" "} + {orgDomain.verified + ? "Verified" + : "Unverified"} + +
+ +
+ ) + )} +
+
+ {(build === "saas" || + build === "enterprise") && ( + + )} + + )} + + {(build === "saas" || + build === "enterprise") && ( + + + + handleBaseDomainSelect({ + id: "provided-search", + domain: + build === + "enterprise" + ? "Provided Domain" + : "Free Provided Domain", + type: "provided-search" + }) + } + className="mx-2 rounded-md" + > +
+ +
+
+ + {build === "enterprise" + ? "Provided Domain" + : "Free Provided Domain"} + + + {t( + "domainPickerSearchForAvailableDomains" + )} + +
+ +
+
+
+ )} +
+
+
+
- {/* Tabs and Sort Toggle */} - {build === "saas" && ( -
- - setActiveTab( - value as "all" | "organization" | "provided" - ) - } - > - - - {t("domainPickerTabAll")} - - - {t("domainPickerTabOrganization")} - - {build == "saas" && ( - - {t("domainPickerTabProvided")} - - )} - - - -
- )} - - {/* Loading State */} - {isChecking && ( -
-
-
- {t("domainPickerCheckingAvailability")} -
-
- )} - - {/* No Options */} - {!isChecking && - filteredOptions.length === 0 && - userInput.trim() && ( - - - - {t("domainPickerNoMatchingDomains")} - - - )} - - {/* Domain Options */} - {!isChecking && filteredOptions.length > 0 && ( + {showProvidedDomainSearch && (
- {/* Organization Domains */} - {organizationOptions.length > 0 && ( -
- {build !== "oss" && ( -
- -

- {t("domainPickerOrganizationDomains")} -

-
- )} -
- {organizationOptions.map((option) => ( -
- option.verified && - handleOptionSelect(option) - } - > -
-
-
-

- {option.domain} -

- {/* */} - {/* {option.domainType} */} - {/* */} - {option.verified ? ( - - ) : ( - - )} -
- {option.subdomain && ( -

- {t( - "domainPickerSubdomain", - { - subdomain: - option.subdomain - } - )} -

- )} - {!option.verified && ( -

- Domain is unverified -

- )} -
-
-
- ))} + {isChecking && ( +
+
+
+ + {t("domainPickerCheckingAvailability")} +
)} - {/* Provided Domains */} - {providedOptions.length > 0 && ( + {!isChecking && + sortedAvailableOptions.length === 0 && + userInput.trim() && ( + + + + {t("domainPickerNoMatchingDomains")} + + + )} + + {!isChecking && sortedAvailableOptions.length > 0 && (
-
- -
- {t("domainPickerProvidedDomains")} -
-
-
- {providedOptions.map((option) => ( -
- handleOptionSelect(option) + { + const option = + displayedProvidedOptions.find( + (opt) => + opt.domainNamespaceId === value + ); + if (option) { + handleProvidedDomainSelect(option); + } + }} + className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} + > + {displayedProvidedOptions.map((option) => ( + ))} -
+ {hasMoreProvided && (
); } diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx new file mode 100644 index 00000000..5d594d02 --- /dev/null +++ b/src/components/EditInternalResourceDialog.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { toast } from "@app/hooks/useToast"; +import { useTranslations } from "next-intl"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Separator } from "@app/components/ui/separator"; + +type InternalResourceData = { + id: number; + name: string; + orgId: string; + siteName: string; + protocol: string; + proxyPort: number | null; + siteId: number; + destinationIp?: string; + destinationPort?: number; +}; + +type EditInternalResourceDialogProps = { + open: boolean; + setOpen: (val: boolean) => void; + resource: InternalResourceData; + orgId: string; + onSuccess?: () => void; +}; + +export default function EditInternalResourceDialog({ + open, + setOpen, + resource, + orgId, + onSuccess +}: EditInternalResourceDialogProps) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const [isSubmitting, setIsSubmitting] = useState(false); + + const formSchema = z.object({ + name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), + protocol: z.enum(["tcp", "udp"]), + proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")), + destinationIp: z.string().ip(t("editInternalResourceDialogInvalidIPAddressFormat")), + destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")) + }); + + type FormData = z.infer; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: resource.name, + protocol: resource.protocol as "tcp" | "udp", + proxyPort: resource.proxyPort || undefined, + destinationIp: resource.destinationIp || "", + destinationPort: resource.destinationPort || undefined + } + }); + + useEffect(() => { + if (open) { + form.reset({ + name: resource.name, + protocol: resource.protocol as "tcp" | "udp", + proxyPort: resource.proxyPort || undefined, + destinationIp: resource.destinationIp || "", + destinationPort: resource.destinationPort || undefined + }); + } + }, [open, resource, form]); + + const handleSubmit = async (data: FormData) => { + setIsSubmitting(true); + try { + // Update the site resource + await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, { + name: data.name, + protocol: data.protocol, + proxyPort: data.proxyPort, + destinationIp: data.destinationIp, + destinationPort: data.destinationPort + }); + + toast({ + title: t("editInternalResourceDialogSuccess"), + description: t("editInternalResourceDialogInternalResourceUpdatedSuccessfully"), + variant: "default" + }); + + onSuccess?.(); + setOpen(false); + } catch (error) { + console.error("Error updating internal resource:", error); + toast({ + title: t("editInternalResourceDialogError"), + description: formatAxiosError(error, t("editInternalResourceDialogFailedToUpdateInternalResource")), + variant: "destructive" + }); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {t("editInternalResourceDialogEditClientResource")} + + {t("editInternalResourceDialogUpdateResourceProperties", { resourceName: resource.name })} + + + +
+ + {/* Resource Properties Form */} +
+

{t("editInternalResourceDialogResourceProperties")}

+
+ ( + + {t("editInternalResourceDialogName")} + + + + + + )} + /> + +
+ ( + + {t("editInternalResourceDialogProtocol")} + + + + )} + /> + + ( + + {t("editInternalResourceDialogSitePort")} + + field.onChange(parseInt(e.target.value) || 0)} + /> + + + + )} + /> +
+
+
+ + {/* Target Configuration Form */} +
+

{t("editInternalResourceDialogTargetConfiguration")}

+
+
+ ( + + {t("editInternalResourceDialogDestinationIP")} + + + + + + )} + /> + + ( + + {t("editInternalResourceDialogDestinationPort")} + + field.onChange(parseInt(e.target.value) || 0)} + /> + + + + )} + /> +
+
+
+
+ +
+ + + + +
+
+ ); +} diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index ce001f09..d309c11f 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -70,7 +70,7 @@ export function LayoutSidebar({ isCollapsed={isSidebarCollapsed} />
-
+
{!isAdminPage && user.serverAdmin && (
diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 7e8ad336..13bd87d3 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -150,7 +150,7 @@ export function SidebarNav({ {section.heading}
)} -
+
{section.items.map((item) => { const hydratedHref = hydrateHref(item.href); const isActive = pathname.startsWith(hydratedHref); diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index e6fad743..2c30ee73 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -9,7 +9,7 @@ const alertVariants = cva( variants: { variant: { default: "bg-card border text-foreground", - neutral: "bg-card border text-foreground", + neutral: "bg-card bg-muted border text-foreground", destructive: "border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive", success: diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index 6b22ddfe..fde1f12b 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -30,7 +30,15 @@ import { CardHeader, CardTitle } from "@app/components/ui/card"; +import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs"; import { useTranslations } from "next-intl"; +import { useMemo } from "react"; + +type TabFilter = { + id: string; + label: string; + filterFn: (row: any) => boolean; +}; type DataTableProps = { columns: ColumnDef[]; @@ -46,6 +54,8 @@ type DataTableProps = { id: string; desc: boolean; }; + tabs?: TabFilter[]; + defaultTab?: string; }; export function DataTable({ @@ -58,17 +68,36 @@ export function DataTable({ isRefreshing, searchPlaceholder = "Search...", searchColumn = "name", - defaultSort + defaultSort, + tabs, + defaultTab }: DataTableProps) { const [sorting, setSorting] = useState( defaultSort ? [defaultSort] : [] ); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState([]); + const [activeTab, setActiveTab] = useState( + defaultTab || tabs?.[0]?.id || "" + ); const t = useTranslations(); + // Apply tab filter to data + const filteredData = useMemo(() => { + if (!tabs || activeTab === "") { + return data; + } + + const activeTabFilter = tabs.find((tab) => tab.id === activeTab); + if (!activeTabFilter) { + return data; + } + + return data.filter(activeTabFilter.filterFn); + }, [data, tabs, activeTab]); + const table = useReactTable({ - data, + data: filteredData, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), @@ -90,20 +119,49 @@ export function DataTable({ } }); + const handleTabChange = (value: string) => { + setActiveTab(value); + // Reset to first page when changing tabs + table.setPageIndex(0); + }; + return (
-
- - table.setGlobalFilter(String(e.target.value)) - } - className="w-full pl-8" - /> - +
+
+ + table.setGlobalFilter( + String(e.target.value) + ) + } + className="w-full pl-8" + /> + +
+ {tabs && tabs.length > 0 && ( + + + {tabs.map((tab) => ( + + {tab.label} ( + {data.filter(tab.filterFn).length}) + + ))} + + + )}
{onRefresh && ( diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx index 9293541d..2afda77d 100644 --- a/src/components/ui/input-otp.tsx +++ b/src/components/ui/input-otp.tsx @@ -55,7 +55,7 @@ function InputOTPSlot({ data-slot="input-otp-slot" data-active={isActive} className={cn( - "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", + "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-2xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", className )} {...props} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 880a44b7..eacaa12e 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -16,7 +16,7 @@ const Input = React.forwardRef( type={showPassword ? "text" : "password"} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm shadow-2xs", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className @@ -43,7 +43,7 @@ const Input = React.forwardRef( type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm shadow-2xs", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index db231e17..03dd3d26 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -36,7 +36,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full", + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-2xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full", className )} {...props} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 7fa26a9e..94050ae2 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef< ) => void; updateAuthInfo: ( diff --git a/src/hooks/useDockerSocket.ts b/src/hooks/useDockerSocket.ts deleted file mode 100644 index dc4f08f4..00000000 --- a/src/hooks/useDockerSocket.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useCallback, useEffect, useState } from "react"; -import { useEnvContext } from "./useEnvContext"; -import { - Container, - GetDockerStatusResponse, - ListContainersResponse, - TriggerFetchResponse -} from "@server/routers/site"; -import { AxiosResponse } from "axios"; -import { toast } from "./useToast"; -import { Site } from "@server/db"; - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export function useDockerSocket(site: Site) { - console.log(`useDockerSocket initialized for site ID: ${site.siteId}`); - - const [dockerSocket, setDockerSocket] = useState(); - const [containers, setContainers] = useState([]); - - const api = createApiClient(useEnvContext()); - - const { dockerSocketEnabled: rawIsEnabled = true, type: siteType } = site || {}; - const isEnabled = rawIsEnabled && siteType === "newt"; - const { isAvailable = false, socketPath } = dockerSocket || {}; - - const checkDockerSocket = useCallback(async () => { - if (!isEnabled) { - console.warn("Docker socket is not enabled for this site."); - return; - } - try { - const res = await api.post(`/site/${site.siteId}/docker/check`); - console.log("Docker socket check response:", res); - } catch (error) { - console.error("Failed to check Docker socket:", error); - } - }, [api, site.siteId, isEnabled]); - - const getDockerSocketStatus = useCallback(async () => { - if (!isEnabled) { - console.warn("Docker socket is not enabled for this site."); - return; - } - - try { - const res = await api.get>( - `/site/${site.siteId}/docker/status` - ); - - if (res.status === 200) { - setDockerSocket(res.data.data); - } else { - console.error("Failed to get Docker status:", res); - toast({ - variant: "destructive", - title: "Failed to get Docker status", - description: - "An error occurred while fetching Docker status." - }); - } - } catch (error) { - console.error("Failed to get Docker status:", error); - toast({ - variant: "destructive", - title: "Failed to get Docker status", - description: "An error occurred while fetching Docker status." - }); - } - }, [api, site.siteId, isEnabled]); - - const getContainers = useCallback( - async (maxRetries: number = 3) => { - if (!isEnabled || !isAvailable) { - console.warn("Docker socket is not enabled or available."); - return; - } - - const fetchContainerList = async () => { - if (!isEnabled || !isAvailable) { - return; - } - - let attempt = 0; - while (attempt < maxRetries) { - try { - const res = await api.get< - AxiosResponse - >(`/site/${site.siteId}/docker/containers`); - setContainers(res.data.data); - return res.data.data; - } catch (error: any) { - attempt++; - - // Check if the error is a 425 (Too Early) status - if (error?.response?.status === 425) { - if (attempt < maxRetries) { - console.log( - `Containers not ready yet (attempt ${attempt}/${maxRetries}). Retrying in 250ms...` - ); - await sleep(250); - continue; - } else { - console.warn( - "Max retry attempts reached. Containers may still be loading." - ); - // toast({ - // variant: "destructive", - // title: "Containers not ready", - // description: - // "Containers are still loading. Please try again in a moment." - // }); - } - } else { - console.error( - "Failed to fetch Docker containers:", - error - ); - toast({ - variant: "destructive", - title: "Failed to fetch containers", - description: formatAxiosError( - error, - "An error occurred while fetching containers" - ) - }); - } - break; - } - } - }; - - try { - const res = await api.post>( - `/site/${site.siteId}/docker/trigger` - ); - // TODO: identify a way to poll the server for latest container list periodically? - await fetchContainerList(); - return res.data.data; - } catch (error) { - console.error("Failed to trigger Docker containers:", error); - } - }, - [api, site.siteId, isEnabled, isAvailable] - ); - - // 2. Docker socket status monitoring - useEffect(() => { - if (!isEnabled || isAvailable) { - return; - } - - checkDockerSocket(); - getDockerSocketStatus(); - - }, [isEnabled, isAvailable, checkDockerSocket, getDockerSocketStatus]); - - return { - isEnabled, - isAvailable: isEnabled && isAvailable, - socketPath, - containers, - check: checkDockerSocket, - status: getDockerSocketStatus, - fetchContainers: getContainers - }; -} diff --git a/src/lib/docker.ts b/src/lib/docker.ts new file mode 100644 index 00000000..d463237b --- /dev/null +++ b/src/lib/docker.ts @@ -0,0 +1,136 @@ +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { + Container, + GetDockerStatusResponse, + ListContainersResponse, + TriggerFetchResponse +} from "@server/routers/site"; +import { AxiosResponse } from "axios"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export interface DockerState { + isEnabled: boolean; + isAvailable: boolean; + socketPath?: string; + containers: Container[]; +} + +export class DockerManager { + private api: any; + private siteId: number; + + constructor(api: any, siteId: number) { + this.api = api; + this.siteId = siteId; + } + + async checkDockerSocket(): Promise { + try { + const res = await this.api.post(`/site/${this.siteId}/docker/check`); + console.log("Docker socket check response:", res); + } catch (error) { + console.error("Failed to check Docker socket:", error); + } + } + + async getDockerSocketStatus(): Promise { + try { + const res = await this.api.get( + `/site/${this.siteId}/docker/status` + ); + + if (res.status === 200) { + return res.data.data as GetDockerStatusResponse; + } else { + console.error("Failed to get Docker status:", res); + return null; + } + } catch (error) { + console.error("Failed to get Docker status:", error); + return null; + } + } + + async fetchContainers(maxRetries: number = 3): Promise { + const fetchContainerList = async (): Promise => { + let attempt = 0; + while (attempt < maxRetries) { + try { + const res = await this.api.get( + `/site/${this.siteId}/docker/containers` + ); + return res.data.data as Container[]; + } catch (error: any) { + attempt++; + + // Check if the error is a 425 (Too Early) status + if (error?.response?.status === 425) { + if (attempt < maxRetries) { + console.log( + `Containers not ready yet (attempt ${attempt}/${maxRetries}). Retrying in 250ms...` + ); + await sleep(250); + continue; + } else { + console.warn( + "Max retry attempts reached. Containers may still be loading." + ); + } + } else { + console.error( + "Failed to fetch Docker containers:", + error + ); + throw error; + } + break; + } + } + return []; + }; + + try { + await this.api.post( + `/site/${this.siteId}/docker/trigger` + ); + return await fetchContainerList(); + } catch (error) { + console.error("Failed to trigger Docker containers:", error); + return []; + } + } + + async initializeDocker(): Promise { + console.log(`Initializing Docker for site ID: ${this.siteId}`); + + // For now, assume Docker is enabled for newt sites + const isEnabled = true; + + if (!isEnabled) { + return { + isEnabled: false, + isAvailable: false, + containers: [] + }; + } + + // Check and get Docker socket status + await this.checkDockerSocket(); + const dockerStatus = await this.getDockerSocketStatus(); + + const isAvailable = dockerStatus?.isAvailable || false; + let containers: Container[] = []; + + if (isAvailable) { + containers = await this.fetchContainers(); + } + + return { + isEnabled, + isAvailable, + socketPath: dockerStatus?.socketPath, + containers + }; + } +} diff --git a/src/providers/ResourceProvider.tsx b/src/providers/ResourceProvider.tsx index 4541035a..da6aca87 100644 --- a/src/providers/ResourceProvider.tsx +++ b/src/providers/ResourceProvider.tsx @@ -3,20 +3,17 @@ import ResourceContext from "@app/contexts/resourceContext"; import { GetResourceAuthInfoResponse } from "@server/routers/resource"; import { GetResourceResponse } from "@server/routers/resource/getResource"; -import { GetSiteResponse } from "@server/routers/site"; import { useState } from "react"; import { useTranslations } from "next-intl"; interface ResourceProviderProps { children: React.ReactNode; resource: GetResourceResponse; - site: GetSiteResponse | null; authInfo: GetResourceAuthInfoResponse; } export function ResourceProvider({ children, - site, resource: serverResource, authInfo: serverAuthInfo }: ResourceProviderProps) { @@ -66,7 +63,7 @@ export function ResourceProvider({ return ( {children} From f9184cf489553e39ab919efb8e14aaa35ee85271 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 20:30:07 -0700 Subject: [PATCH 125/219] Handle badger config correctly --- server/lib/remoteTraefikConfig.ts | 37 +++++++++++--- server/routers/traefik/getTraefikConfig.ts | 57 +++++++++++----------- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index 88ea011a..bca59ab2 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -213,9 +213,34 @@ export class TraefikConfigManager { } } - logger.debug( - `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` - ); + const badgerMiddlewareName = "badger"; + traefikConfig.http.middlewares[badgerMiddlewareName] = { + plugin: { + [badgerMiddlewareName]: { + apiBaseUrl: new URL( + "/api/v0", + `http://${ + config.getRawConfig().server.internal_hostname + }:${config.getRawConfig().server.internal_port}` + ).href, + userSessionCookieName: + config.getRawConfig().server.session_cookie_name, + + // deprecated + accessTokenQueryParam: + config.getRawConfig().server + .resource_access_token_param, + + resourceSessionRequestParam: + config.getRawConfig().server + .resource_session_request_param + } + } + }; + + // logger.debug( + // `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` + // ); return { domains, traefikConfig }; } catch (error) { @@ -313,9 +338,9 @@ export class TraefikConfigManager { return []; } - logger.debug( - `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains` - ); + // logger.debug( + // `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains` + // ); return response.data.data; } catch (error) { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 89afee2c..a0e22acb 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -8,6 +8,8 @@ import { orgs, resources, sites, Target, targets } from "@server/db"; import { build } from "@server/build"; let currentExitNodeId: number; +const redirectHttpsMiddlewareName = "redirect-to-https"; +const badgerMiddlewareName = "badger"; export async function traefikConfigProvider( _: Request, @@ -43,7 +45,32 @@ export async function traefikConfigProvider( } } - const traefikConfig = await getTraefikConfig(currentExitNodeId); + let traefikConfig = await getTraefikConfig(currentExitNodeId); + + traefikConfig.http.middlewares[badgerMiddlewareName] = { + plugin: { + [badgerMiddlewareName]: { + apiBaseUrl: new URL( + "/api/v0", + `http://${ + config.getRawConfig().server.internal_hostname + }:${config.getRawConfig().server.internal_port}` + ).href, + userSessionCookieName: + config.getRawConfig().server.session_cookie_name, + + // deprecated + accessTokenQueryParam: + config.getRawConfig().server + .resource_access_token_param, + + resourceSessionRequestParam: + config.getRawConfig().server + .resource_session_request_param + } + } + }; + return res.status(HttpCode.OK).json(traefikConfig); } catch (e) { logger.error(`Failed to build Traefik config: ${e}`); @@ -132,37 +159,9 @@ export async function getTraefikConfig(exitNodeId: number): Promise { return {}; } - const badgerMiddlewareName = "badger"; - const redirectHttpsMiddlewareName = "redirect-to-https"; - const config_output: any = { http: { middlewares: { - [badgerMiddlewareName]: { - plugin: { - [badgerMiddlewareName]: { - apiBaseUrl: new URL( - "/api/v1", - `http://${ - config.getRawConfig().server - .internal_hostname - }:${config.getRawConfig().server.internal_port}` - ).href, - userSessionCookieName: - config.getRawConfig().server - .session_cookie_name, - - // deprecated - accessTokenQueryParam: - config.getRawConfig().server - .resource_access_token_param, - - resourceSessionRequestParam: - config.getRawConfig().server - .resource_session_request_param - } - } - }, [redirectHttpsMiddlewareName]: { redirectScheme: { scheme: "https" From 825bff5d6018ab2a529e97fb039aedcc52338e80 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 14 Aug 2025 21:48:14 -0700 Subject: [PATCH 126/219] Badger & traefik working now? --- server/auth/sessions/resource.ts | 26 ++++++ server/db/queries/verifySessionQueries.ts | 104 +++++++++++++++++---- server/lib/readConfigFile.ts | 3 +- server/lib/remoteTraefikConfig.ts | 2 +- server/routers/badger/verifySession.ts | 12 ++- server/routers/internal.ts | 2 +- server/routers/traefik/getTraefikConfig.ts | 2 +- 7 files changed, 126 insertions(+), 25 deletions(-) diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index f29a8b75..8d676bec 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -4,6 +4,9 @@ import { resourceSessions, ResourceSession } from "@server/db"; import { db } from "@server/db"; import { eq, and } from "drizzle-orm"; import config from "@server/lib/config"; +import axios from "axios"; +import logger from "@server/logger"; +import { tokenManager } from "@server/lib/tokenManager"; export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name; @@ -62,6 +65,29 @@ export async function validateResourceSessionToken( token: string, resourceId: number ): Promise { + if (config.isHybridMode()) { + try { + const response = await axios.post(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, { + token: token + }, await tokenManager.getAuthHeader()); + return response.data.data; + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error("Error validating resource session token in hybrid mode:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error validating resource session token in hybrid mode:", error); + } + return { resourceSession: null }; + } + } + const sessionId = encodeHexLowerCase( sha256(new TextEncoder().encode(token)) ); diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 44982f64..4c800125 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -17,6 +17,8 @@ import { import { and, eq } from "drizzle-orm"; import axios from "axios"; import config from "@server/lib/config"; +import logger from "@server/logger"; +import { tokenManager } from "@server/lib/tokenManager"; export type ResourceWithAuth = { resource: Resource | null; @@ -37,10 +39,21 @@ export async function getResourceByDomain( ): Promise { if (config.isHybridMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/resource/domain/${domain}`); - return response.data; + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/resource/domain/${domain}`, await tokenManager.getAuthHeader()); + return response.data.data; } catch (error) { - console.error("Error fetching resource by domain:", error); + if (axios.isAxiosError(error)) { + logger.error("Error fetching config in verify session:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error fetching config in verify session:", error); + } return null; } } @@ -78,10 +91,21 @@ export async function getUserSessionWithUser( ): Promise { if (config.isHybridMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/session/${userSessionId}`); - return response.data; + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/session/${userSessionId}`, await tokenManager.getAuthHeader()); + return response.data.data; } catch (error) { - console.error("Error fetching user session:", error); + if (axios.isAxiosError(error)) { + logger.error("Error fetching config in verify session:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error fetching config in verify session:", error); + } return null; } } @@ -108,10 +132,21 @@ export async function getUserSessionWithUser( export async function getUserOrgRole(userId: string, orgId: string) { if (config.isHybridMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/user/${userId}/org/${orgId}/role`); - return response.data; + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`, await tokenManager.getAuthHeader()); + return response.data.data; } catch (error) { - console.error("Error fetching user org role:", error); + if (axios.isAxiosError(error)) { + logger.error("Error fetching config in verify session:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error fetching config in verify session:", error); + } return null; } } @@ -136,10 +171,21 @@ export async function getUserOrgRole(userId: string, orgId: string) { export async function getRoleResourceAccess(resourceId: number, roleId: number) { if (config.isHybridMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/role/${roleId}/resource/${resourceId}/access`); - return response.data; + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader()); + return response.data.data; } catch (error) { - console.error("Error fetching role resource access:", error); + if (axios.isAxiosError(error)) { + logger.error("Error fetching config in verify session:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error fetching config in verify session:", error); + } return null; } } @@ -164,10 +210,21 @@ export async function getRoleResourceAccess(resourceId: number, roleId: number) export async function getUserResourceAccess(userId: string, resourceId: number) { if (config.isHybridMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/user/${userId}/resource/${resourceId}/access`); - return response.data; + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader()); + return response.data.data; } catch (error) { - console.error("Error fetching user resource access:", error); + if (axios.isAxiosError(error)) { + logger.error("Error fetching config in verify session:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error fetching config in verify session:", error); + } return null; } } @@ -192,10 +249,21 @@ export async function getUserResourceAccess(userId: string, resourceId: number) export async function getResourceRules(resourceId: number): Promise { if (config.isHybridMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/resource/${resourceId}/rules`); - return response.data; + const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`, await tokenManager.getAuthHeader()); + return response.data.data; } catch (error) { - console.error("Error fetching resource rules:", error); + if (axios.isAxiosError(error)) { + logger.error("Error fetching config in verify session:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error fetching config in verify session:", error); + } return []; } } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 5fb7b955..93a716c5 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -32,7 +32,8 @@ export const configSchema = z .object({ id: z.string().optional(), secret: z.string().optional(), - endpoint: z.string().optional() + endpoint: z.string().optional(), + redirect_endpoint: z.string().optional() }) .optional(), domains: z diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index bca59ab2..e192ab67 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -218,7 +218,7 @@ export class TraefikConfigManager { plugin: { [badgerMiddlewareName]: { apiBaseUrl: new URL( - "/api/v0", + "/api/v1", `http://${ config.getRawConfig().server.internal_hostname }:${config.getRawConfig().server.internal_port}` diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 54a2e0c9..50b9ed68 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -140,7 +140,7 @@ export async function verifyResourceSession( const result = await getResourceByDomain(cleanHost); if (!result) { - logger.debug("Resource not found", cleanHost); + logger.debug(`Resource not found ${cleanHost}`); return notAllowed(res); } @@ -151,7 +151,7 @@ export async function verifyResourceSession( const { resource, pincode, password } = resourceData; if (!resource) { - logger.debug("Resource not found", cleanHost); + logger.debug(`Resource not found ${cleanHost}`); return notAllowed(res); } @@ -191,7 +191,13 @@ export async function verifyResourceSession( return allowed(res); } - const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent( + let endpoint: string; + if (config.isHybridMode()) { + endpoint = config.getRawConfig().hybrid?.redirect_endpoint || config.getRawConfig().hybrid?.endpoint || ""; + } else { + endpoint = config.getRawConfig().app.dashboard_url; + } + const redirectUrl = `${endpoint}/auth/resource/${encodeURIComponent( resource.resourceId )}?redirect=${encodeURIComponent(originalRequestURL)}`; diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 977248e5..d19355b7 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -87,7 +87,7 @@ badgerRouter.post("/verify-session", badger.verifyResourceSession); if (config.isHybridMode()) { badgerRouter.post("/exchange-session", (req, res, next) => - proxyToRemote(req, res, next, "badger/exchange-session") + proxyToRemote(req, res, next, "hybrid/badger/exchange-session") ); } else { badgerRouter.post("/exchange-session", badger.exchangeSession); diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index a0e22acb..ac1369c9 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -51,7 +51,7 @@ export async function traefikConfigProvider( plugin: { [badgerMiddlewareName]: { apiBaseUrl: new URL( - "/api/v0", + "/api/v1", `http://${ config.getRawConfig().server.internal_hostname }:${config.getRawConfig().server.internal_port}` From b56db41d0b71c6eeed1db089a66561119d5b8f3f Mon Sep 17 00:00:00 2001 From: Pallavi Date: Fri, 15 Aug 2025 21:23:07 +0530 Subject: [PATCH 127/219] add missing hostmeta export for PG schema --- server/db/pg/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 2ba10e3e..06540f5e 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -648,3 +648,4 @@ export type UserClient = InferSelectModel; export type RoleClient = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; +export type HostMeta = InferSelectModel; From 2fea091e1f08b7a8b740aab66473504627b75eef Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 Aug 2025 12:24:54 -0700 Subject: [PATCH 128/219] Move newt version --- .../routers/newt/handleNewtRegisterMessage.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 71a6fd5c..bb982c24 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -64,16 +64,6 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { exitNodeId = bestPingResult.exitNodeId; } - if (newtVersion) { - // update the newt version in the database - await db - .update(newts) - .set({ - version: newtVersion as string - }) - .where(eq(newts.newtId, newt.newtId)); - } - const [oldSite] = await db .select() .from(sites) @@ -160,6 +150,16 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { allowedIps: [siteSubnet] }); + if (newtVersion && newtVersion !== newt.version) { + // update the newt version in the database + await db + .update(newts) + .set({ + version: newtVersion as string + }) + .where(eq(newts.newtId, newt.newtId)); + } + // Improved version const allResources = await db.transaction(async (tx) => { // First get all resources for the site From 69a9bcb3da20d99ca355cb171bbb1ce90ee01816 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 Aug 2025 15:34:31 -0700 Subject: [PATCH 129/219] Add exit node helper functions --- server/lib/exitNodes/exitNodes.ts | 43 +++++++++++++++++++++++++++++++ server/lib/exitNodes/index.ts | 1 + 2 files changed, 44 insertions(+) create mode 100644 server/lib/exitNodes/exitNodes.ts create mode 100644 server/lib/exitNodes/index.ts diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts new file mode 100644 index 00000000..7b25873e --- /dev/null +++ b/server/lib/exitNodes/exitNodes.ts @@ -0,0 +1,43 @@ +import { db, exitNodes } from "@server/db"; +import logger from "@server/logger"; +import { eq, and, or } from "drizzle-orm"; + +export async function privateVerifyExitNodeOrgAccess( + exitNodeId: number, + orgId: string +) { + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, exitNodeId)); + + // For any other type, deny access + return { hasAccess: true, exitNode }; +} + +export async function listExitNodes(orgId: string, filterOnline = false) { + // TODO: pick which nodes to send and ping better than just all of them that are not remote + const allExitNodes = await db + .select({ + exitNodeId: exitNodes.exitNodeId, + name: exitNodes.name, + address: exitNodes.address, + endpoint: exitNodes.endpoint, + publicKey: exitNodes.publicKey, + listenPort: exitNodes.listenPort, + reachableAt: exitNodes.reachableAt, + maxConnections: exitNodes.maxConnections, + online: exitNodes.online, + lastPing: exitNodes.lastPing, + type: exitNodes.type, + }) + .from(exitNodes); + + // Filter the nodes. If there are NO remoteExitNodes then do nothing. If there are then remove all of the non-remoteExitNodes + if (allExitNodes.length === 0) { + logger.warn("No exit nodes found!"); + return []; + } + + return allExitNodes; +} diff --git a/server/lib/exitNodes/index.ts b/server/lib/exitNodes/index.ts new file mode 100644 index 00000000..b29bce93 --- /dev/null +++ b/server/lib/exitNodes/index.ts @@ -0,0 +1 @@ +export * from "./privateExitNodes"; \ No newline at end of file From 5c94887949a5e6e2be68643b13e5ece0cc51a6d1 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 Aug 2025 15:45:45 -0700 Subject: [PATCH 130/219] Use new exit node functions --- server/lib/exitNodes/exitNodes.ts | 2 +- server/routers/client/createClient.ts | 20 +++------- .../newt/handleNewtPingRequestMessage.ts | 24 +++++++---- .../routers/newt/handleNewtRegisterMessage.ts | 25 ++++++++---- server/routers/site/createSite.ts | 29 +++++++++++++- server/routers/site/pickSiteDefaults.ts | 40 +++++++++---------- 6 files changed, 87 insertions(+), 53 deletions(-) diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts index 7b25873e..f5854e27 100644 --- a/server/lib/exitNodes/exitNodes.ts +++ b/server/lib/exitNodes/exitNodes.ts @@ -2,7 +2,7 @@ import { db, exitNodes } from "@server/db"; import logger from "@server/logger"; import { eq, and, or } from "drizzle-orm"; -export async function privateVerifyExitNodeOrgAccess( +export async function verifyExitNodeOrgAccess( exitNodeId: number, orgId: string ) { diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 4e9dcdce..e7762223 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -24,6 +24,7 @@ import { hashPassword } from "@server/auth/password"; import { isValidCIDR, isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; import { OpenAPITags, registry } from "@server/openApi"; +import { listExitNodes } from "@server/lib/exitNodes"; const createClientParamsSchema = z .object({ @@ -177,20 +178,9 @@ export async function createClient( await db.transaction(async (trx) => { // TODO: more intelligent way to pick the exit node - - // make sure there is an exit node by counting the exit nodes table - const nodes = await db.select().from(exitNodes); - if (nodes.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "No exit nodes available" - ) - ); - } - - // get the first exit node - const exitNode = nodes[0]; + const exitNodesList = await listExitNodes(orgId); + const randomExitNode = + exitNodesList[Math.floor(Math.random() * exitNodesList.length)]; const adminRole = await trx .select() @@ -208,7 +198,7 @@ export async function createClient( const [newClient] = await trx .insert(clients) .values({ - exitNodeId: exitNode.exitNodeId, + exitNodeId: randomExitNode.exitNodeId, orgId, name, subnet: updatedSubnet, diff --git a/server/routers/newt/handleNewtPingRequestMessage.ts b/server/routers/newt/handleNewtPingRequestMessage.ts index 65edea61..f93862f6 100644 --- a/server/routers/newt/handleNewtPingRequestMessage.ts +++ b/server/routers/newt/handleNewtPingRequestMessage.ts @@ -4,6 +4,7 @@ import { exitNodes, Newt } from "@server/db"; import logger from "@server/logger"; import config from "@server/lib/config"; import { ne, eq, or, and, count } from "drizzle-orm"; +import { listExitNodes } from "@server/lib/exitNodes"; export const handleNewtPingRequestMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; @@ -16,12 +17,19 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => { return; } - // TODO: pick which nodes to send and ping better than just all of them - let exitNodesList = await db - .select() - .from(exitNodes); + // Get the newt's orgId through the site relationship + if (!newt.siteId) { + logger.warn("Newt siteId not found"); + return; + } - exitNodesList = exitNodesList.filter((node) => node.maxConnections !== 0); + const [site] = await db + .select({ orgId: sites.orgId }) + .from(sites) + .where(eq(sites.siteId, newt.siteId)) + .limit(1); + + const exitNodesList = await listExitNodes(site.orgId, true); // filter for only the online ones let lastExitNodeId = null; if (newt.siteId) { @@ -54,9 +62,9 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => { ) ); - if (currentConnections.count >= maxConnections) { - return null; - } + if (currentConnections.count >= maxConnections) { + return null; + } weight = (maxConnections - currentConnections.count) / diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index bb982c24..26aa3477 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -9,6 +9,7 @@ import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip"; +import { verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; export type ExitNodePingResult = { exitNodeId: number; @@ -24,7 +25,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; const newt = client as Newt; - logger.info("Handling register newt message!"); + logger.debug("Handling register newt message!"); if (!newt) { logger.warn("Newt not found"); @@ -81,6 +82,18 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { // This effectively moves the exit node to the new one exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId + const { exitNode, hasAccess } = await verifyExitNodeOrgAccess(exitNodeIdToQuery, oldSite.orgId); + + if (!exitNode) { + logger.warn("Exit node not found"); + return; + } + + if (!hasAccess) { + logger.warn("Not authorized to use this exit node"); + return; + } + const sitesQuery = await db .select({ subnet: sites.subnet @@ -88,14 +101,10 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { .from(sites) .where(eq(sites.exitNodeId, exitNodeId)); - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, exitNodeIdToQuery)) - .limit(1); - const blockSize = config.getRawConfig().gerbil.site_block_size; - const subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null); + const subnets = sitesQuery + .map((site) => site.subnet) + .filter((subnet) => subnet !== null); subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`)); const newSubnet = findNextAvailableCidr( subnets, diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index fb1170cd..af8e4073 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { clients, db } from "@server/db"; +import { clients, db, exitNodes } from "@server/db"; import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -17,6 +17,7 @@ import { hashPassword } from "@server/auth/password"; import { isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; import config from "@server/lib/config"; +import { verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; const createSiteParamsSchema = z .object({ @@ -217,6 +218,32 @@ export async function createSite( ); } + const { exitNode, hasAccess } = + await verifyExitNodeOrgAccess( + exitNodeId, + orgId + ); + + if (!exitNode) { + logger.warn("Exit node not found"); + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Exit node not found" + ) + ); + } + + if (!hasAccess) { + logger.warn("Not authorized to use this exit node"); + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Not authorized to use this exit node" + ) + ); + } + [newSite] = await trx .insert(sites) .values({ diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index d6309d0c..2e705c56 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -6,12 +6,16 @@ import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip"; +import { + findNextAvailableCidr, + getNextAvailableClientSubnet +} from "@server/lib/ip"; import { generateId } from "@server/auth/sessions/app"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; import { fromError } from "zod-validation-error"; import { z } from "zod"; +import { listExitNodes } from "@server/lib/exitNodes"; export type PickSiteDefaultsResponse = { exitNodeId: number; @@ -65,16 +69,10 @@ export async function pickSiteDefaults( const { orgId } = parsedParams.data; // TODO: more intelligent way to pick the exit node - // make sure there is an exit node by counting the exit nodes table - const nodes = await db.select().from(exitNodes); - if (nodes.length === 0) { - return next( - createHttpError(HttpCode.NOT_FOUND, "No exit nodes available") - ); - } + const exitNodesList = await listExitNodes(orgId); - // get the first exit node - const exitNode = nodes[0]; + const randomExitNode = + exitNodesList[Math.floor(Math.random() * exitNodesList.length)]; // TODO: this probably can be optimized... // list all of the sites on that exit node @@ -83,13 +81,15 @@ export async function pickSiteDefaults( subnet: sites.subnet }) .from(sites) - .where(eq(sites.exitNodeId, exitNode.exitNodeId)); + .where(eq(sites.exitNodeId, randomExitNode.exitNodeId)); // TODO: we need to lock this subnet for some time so someone else does not take it - const subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null); + const subnets = sitesQuery + .map((site) => site.subnet) + .filter((subnet) => subnet !== null); // exclude the exit node address by replacing after the / with a site block size subnets.push( - exitNode.address.replace( + randomExitNode.address.replace( /\/\d+$/, `/${config.getRawConfig().gerbil.site_block_size}` ) @@ -97,7 +97,7 @@ export async function pickSiteDefaults( const newSubnet = findNextAvailableCidr( subnets, config.getRawConfig().gerbil.site_block_size, - exitNode.address + randomExitNode.address ); if (!newSubnet) { return next( @@ -125,12 +125,12 @@ export async function pickSiteDefaults( return response(res, { data: { - exitNodeId: exitNode.exitNodeId, - address: exitNode.address, - publicKey: exitNode.publicKey, - name: exitNode.name, - listenPort: exitNode.listenPort, - endpoint: exitNode.endpoint, + exitNodeId: randomExitNode.exitNodeId, + address: randomExitNode.address, + publicKey: randomExitNode.publicKey, + name: randomExitNode.name, + listenPort: randomExitNode.listenPort, + endpoint: randomExitNode.endpoint, // subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().gerbil.block_size}`, // we want the block size of the whole subnet subnet: newSubnet, clientAddress: clientAddress, From 21ce678e5b8a1be75c25224295088d6b36b830c6 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 Aug 2025 15:52:09 -0700 Subject: [PATCH 131/219] Move exit node function --- server/lib/exitNodes/exitNodes.ts | 12 ++++++++++++ server/routers/newt/handleNewtRegisterMessage.ts | 15 ++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts index f5854e27..f49b9cdb 100644 --- a/server/lib/exitNodes/exitNodes.ts +++ b/server/lib/exitNodes/exitNodes.ts @@ -1,5 +1,6 @@ import { db, exitNodes } from "@server/db"; import logger from "@server/logger"; +import { ExitNodePingResult } from "@server/routers/newt"; import { eq, and, or } from "drizzle-orm"; export async function verifyExitNodeOrgAccess( @@ -41,3 +42,14 @@ export async function listExitNodes(orgId: string, filterOnline = false) { return allExitNodes; } + +export function selectBestExitNode( + pingResults: ExitNodePingResult[] +): ExitNodePingResult | null { + if (!pingResults || pingResults.length === 0) { + logger.warn("No ping results provided"); + return null; + } + + return pingResults[0]; +} diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 26aa3477..b274a474 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -9,7 +9,7 @@ import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip"; -import { verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; +import { selectBestExitNode, verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; export type ExitNodePingResult = { exitNodeId: number; @@ -265,15 +265,4 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { broadcast: false, // Send to all clients excludeSender: false // Include sender in broadcast }; -}; - -function selectBestExitNode( - pingResults: ExitNodePingResult[] -): ExitNodePingResult | null { - if (!pingResults || pingResults.length === 0) { - logger.warn("No ping results provided"); - return null; - } - - return pingResults[0]; -} +}; \ No newline at end of file From e043d0e654093752bf62feab9df7f556f9c96c82 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 Aug 2025 15:59:38 -0700 Subject: [PATCH 132/219] Use new function --- server/routers/olm/handleOlmRegisterMessage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 64443e07..c892b051 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -4,6 +4,7 @@ import { clients, clientSites, exitNodes, Olm, olms, sites } from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; +import { listExitNodes } from "@server/lib/exitNodes"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.info("Handling register olm message!"); @@ -48,7 +49,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // TODO: FOR NOW WE ARE JUST HOLEPUNCHING ALL EXIT NODES BUT IN THE FUTURE WE SHOULD HANDLE THIS BETTER // Get the exit node - const allExitNodes = await db.select().from(exitNodes); + const allExitNodes = await listExitNodes(client.orgId, true); // FILTER THE ONLINE ONES const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => { return { From 200a7fcd40338da62e1c6740f6b06588dafd4c2c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 15 Aug 2025 16:00:58 -0700 Subject: [PATCH 133/219] fix sidebar spacing --- src/components/SidebarNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 13bd87d3..7e8ad336 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -150,7 +150,7 @@ export function SidebarNav({ {section.heading}
)} -
+
{section.items.map((item) => { const hydratedHref = hydrateHref(item.href); const isActive = pathname.startsWith(hydratedHref); From e73383cc79a05922fe313b4c72971b620e4cd090 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 Aug 2025 16:53:30 -0700 Subject: [PATCH 134/219] Add auth to gerbil calls --- server/lib/exitNodes/exitNodes.ts | 7 ++++ server/routers/gerbil/receiveBandwidth.ts | 44 +++++++++++++++++++---- server/routers/gerbil/updateHolePunch.ts | 23 +++++++++++- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts index f49b9cdb..f607371d 100644 --- a/server/lib/exitNodes/exitNodes.ts +++ b/server/lib/exitNodes/exitNodes.ts @@ -53,3 +53,10 @@ export function selectBestExitNode( return pingResults[0]; } + +export async function checkExitNodeOrg( + exitNodeId: number, + orgId: string +) { + return false; +} \ No newline at end of file diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index 350228ec..fb7723ee 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -6,6 +6,7 @@ import logger from "@server/logger"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; +import { checkExitNodeOrg } from "@server/lib/exitNodes"; // Track sites that are already offline to avoid unnecessary queries const offlineSites = new Set(); @@ -48,7 +49,10 @@ export const receiveBandwidth = async ( } }; -export async function updateSiteBandwidth(bandwidthData: PeerBandwidth[]) { +export async function updateSiteBandwidth( + bandwidthData: PeerBandwidth[], + exitNodeId?: number +) { const currentTime = new Date(); const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago @@ -69,7 +73,7 @@ export async function updateSiteBandwidth(bandwidthData: PeerBandwidth[]) { // Update all active sites with bandwidth data and get the site data in one operation const updatedSites = []; for (const peer of activePeers) { - const updatedSite = await trx + const [updatedSite] = await trx .update(sites) .set({ megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`, @@ -85,8 +89,19 @@ export async function updateSiteBandwidth(bandwidthData: PeerBandwidth[]) { lastBandwidthUpdate: sites.lastBandwidthUpdate }); - if (updatedSite.length > 0) { - updatedSites.push({ ...updatedSite[0], peer }); + if (exitNodeId) { + if (await checkExitNodeOrg(exitNodeId, updatedSite.orgId)) { + // not allowed + logger.warn( + `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` + ); + // THIS SHOULD TRIGGER THE TRANSACTION TO FAIL? + throw new Error("Exit node not allowed"); + } + } + + if (updatedSite) { + updatedSites.push({ ...updatedSite, peer }); } } @@ -138,12 +153,29 @@ export async function updateSiteBandwidth(bandwidthData: PeerBandwidth[]) { // Always update lastBandwidthUpdate to show this instance is receiving reports // Only update online status if it changed if (site.online !== newOnlineStatus) { - await trx + const [updatedSite] = await trx .update(sites) .set({ online: newOnlineStatus }) - .where(eq(sites.siteId, site.siteId)); + .where(eq(sites.siteId, site.siteId)) + .returning(); + + if (exitNodeId) { + if ( + await checkExitNodeOrg( + exitNodeId, + updatedSite.orgId + ) + ) { + // not allowed + logger.warn( + `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` + ); + // THIS SHOULD TRIGGER THE TRANSACTION TO FAIL? + throw new Error("Exit node not allowed"); + } + } // If site went offline, add it to our tracking set if (!newOnlineStatus && site.pubKey) { diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 0eaa447e..9e2ec8b8 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -19,6 +19,7 @@ import { fromError } from "zod-validation-error"; import { validateNewtSessionToken } from "@server/auth/sessions/newt"; import { validateOlmSessionToken } from "@server/auth/sessions/olm"; import axios from "axios"; +import { checkExitNodeOrg } from "@server/lib/exitNodes"; // Define Zod schema for request validation const updateHolePunchSchema = z.object({ @@ -157,7 +158,13 @@ export async function updateAndGenerateEndpointDestinations( .where(eq(clients.clientId, olm.clientId)) .returning(); - + if (await checkExitNodeOrg(exitNode.exitNodeId, client.orgId)) { + // not allowed + logger.warn( + `Exit node ${exitNode.exitNodeId} is not allowed for org ${client.orgId}` + ); + throw new Error("Exit node not allowed"); + } // Get sites that are on this specific exit node and connected to this client const sitesOnExitNode = await db @@ -240,6 +247,20 @@ export async function updateAndGenerateEndpointDestinations( throw new Error("Newt not found"); } + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, newt.siteId)) + .limit(1); + + if (await checkExitNodeOrg(exitNode.exitNodeId, site.orgId)) { + // not allowed + logger.warn( + `Exit node ${exitNode.exitNodeId} is not allowed for org ${site.orgId}` + ); + throw new Error("Exit node not allowed"); + } + currentSiteId = newt.siteId; // Update the current site with the new endpoint From 48963f24df30b6b6813d6fe5c09fda6315ce7821 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sat, 16 Aug 2025 12:06:42 -0700 Subject: [PATCH 135/219] add oss check --- server/lib/telemetry.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts index 8475fb34..ed3a8e73 100644 --- a/server/lib/telemetry.ts +++ b/server/lib/telemetry.ts @@ -8,6 +8,7 @@ import { eq, count, notInArray } from "drizzle-orm"; import { APP_VERSION } from "./consts"; import crypto from "crypto"; import { UserType } from "@server/types/UserTypes"; +import { build } from "@server/build"; class TelemetryClient { private client: PostHog | null = null; @@ -19,7 +20,15 @@ class TelemetryClient { this.enabled = enabled; const dev = process.env.ENVIRONMENT !== "prod"; - if (this.enabled && !dev) { + if (dev) { + return; + } + + if (build !== "oss") { + return; + } + + if (this.enabled) { this.client = new PostHog( "phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX", { @@ -40,7 +49,7 @@ class TelemetryClient { logger.info( "Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry" ); - } else if (!this.enabled && !dev) { + } else if (!this.enabled) { logger.info( "Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.digpangolin.com/telemetry" ); From f07cd8aee3411bb09a241c66544a6ce35e4cfcb7 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 16 Aug 2025 12:07:15 -0700 Subject: [PATCH 136/219] Fix traefik config merge --- server/routers/traefik/getTraefikConfig.ts | 349 +++++---------------- 1 file changed, 81 insertions(+), 268 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 325c4205..7349df02 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -94,94 +94,94 @@ export async function getTraefikConfig(exitNodeId: number): Promise { // Get all resources with related data const allResources = await db.transaction(async (tx) => { // Get resources with their targets and sites in a single optimized query - // Start from sites on this exit node, then join to targets and resources - const resourcesWithTargetsAndSites = await tx - .select({ - // Resource fields - resourceId: resources.resourceId, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol, - subdomain: resources.subdomain, - domainId: resources.domainId, - enabled: resources.enabled, - stickySession: resources.stickySession, - tlsServerName: resources.tlsServerName, - setHostHeader: resources.setHostHeader, - enableProxy: resources.enableProxy, - // Target fields - targetId: targets.targetId, - targetEnabled: targets.enabled, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - // Site fields - siteId: sites.siteId, - siteType: sites.type, - subnet: sites.subnet, - exitNodeId: sites.exitNodeId - }) - .from(sites) - .innerJoin(targets, eq(targets.siteId, sites.siteId)) - .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) - .where( - and( - eq(targets.enabled, true), - eq(resources.enabled, true), - or( - eq(sites.exitNodeId, currentExitNodeId), - isNull(sites.exitNodeId) - ) + // Start from sites on this exit node, then join to targets and resources + const resourcesWithTargetsAndSites = await tx + .select({ + // Resource fields + resourceId: resources.resourceId, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + http: resources.http, + proxyPort: resources.proxyPort, + protocol: resources.protocol, + subdomain: resources.subdomain, + domainId: resources.domainId, + enabled: resources.enabled, + stickySession: resources.stickySession, + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader, + enableProxy: resources.enableProxy, + // Target fields + targetId: targets.targetId, + targetEnabled: targets.enabled, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + // Site fields + siteId: sites.siteId, + siteType: sites.type, + subnet: sites.subnet, + exitNodeId: sites.exitNodeId + }) + .from(sites) + .innerJoin(targets, eq(targets.siteId, sites.siteId)) + .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) + .where( + and( + eq(targets.enabled, true), + eq(resources.enabled, true), + or( + eq(sites.exitNodeId, currentExitNodeId), + isNull(sites.exitNodeId) ) - ); + ) + ); - // Group by resource and include targets with their unique site data - const resourcesMap = new Map(); + // Group by resource and include targets with their unique site data + const resourcesMap = new Map(); - resourcesWithTargetsAndSites.forEach((row) => { - const resourceId = row.resourceId; + resourcesWithTargetsAndSites.forEach((row) => { + const resourceId = row.resourceId; - if (!resourcesMap.has(resourceId)) { - resourcesMap.set(resourceId, { - resourceId: row.resourceId, - fullDomain: row.fullDomain, - ssl: row.ssl, - http: row.http, - proxyPort: row.proxyPort, - protocol: row.protocol, - subdomain: row.subdomain, - domainId: row.domainId, - enabled: row.enabled, - stickySession: row.stickySession, - tlsServerName: row.tlsServerName, - setHostHeader: row.setHostHeader, - enableProxy: row.enableProxy, - targets: [] - }); - } - - // Add target with its associated site data - resourcesMap.get(resourceId).targets.push({ + if (!resourcesMap.has(resourceId)) { + resourcesMap.set(resourceId, { resourceId: row.resourceId, - targetId: row.targetId, - ip: row.ip, - method: row.method, - port: row.port, - internalPort: row.internalPort, - enabled: row.targetEnabled, - site: { - siteId: row.siteId, - type: row.siteType, - subnet: row.subnet, - exitNodeId: row.exitNodeId - } + fullDomain: row.fullDomain, + ssl: row.ssl, + http: row.http, + proxyPort: row.proxyPort, + protocol: row.protocol, + subdomain: row.subdomain, + domainId: row.domainId, + enabled: row.enabled, + stickySession: row.stickySession, + tlsServerName: row.tlsServerName, + setHostHeader: row.setHostHeader, + enableProxy: row.enableProxy, + targets: [] }); - }); + } - return Array.from(resourcesMap.values()); + // Add target with its associated site data + resourcesMap.get(resourceId).targets.push({ + resourceId: row.resourceId, + targetId: row.targetId, + ip: row.ip, + method: row.method, + port: row.port, + internalPort: row.internalPort, + enabled: row.targetEnabled, + site: { + siteId: row.siteId, + type: row.siteType, + subnet: row.subnet, + exitNodeId: row.exitNodeId + } + }); + }); + + return Array.from(resourcesMap.values()); }); if (!allResources.length) { @@ -299,194 +299,7 @@ export async function getTraefikConfig(exitNodeId: number): Promise { middlewares: [redirectHttpsMiddlewareName], service: serviceName, rule: `Host(\`${fullDomain}\`)`, -<<<<<<< HEAD priority: 100 -======= - priority: 100, - ...(resource.ssl ? { tls } : {}) - }; - - if (resource.ssl) { - config_output.http.routers![routerName + "-redirect"] = { - entryPoints: [ - config.getRawConfig().traefik.http_entrypoint - ], - middlewares: [redirectHttpsMiddlewareName], - service: serviceName, - rule: `Host(\`${fullDomain}\`)`, - priority: 100 - }; - } - - config_output.http.services![serviceName] = { - loadBalancer: { - servers: targets - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - if ( - !target.ip || - !target.port || - !target.method - ) { - return false; - } - } else if (target.site.type === "newt") { - if ( - !target.internalPort || - !target.method || - !target.site.subnet - ) { - return false; - } - } - return true; - }) - .map((target: TargetWithSite) => { - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - return { - url: `${target.method}://${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { - const ip = target.site.subnet!.split("/")[0]; - return { - url: `${target.method}://${ip}:${target.internalPort}` - }; - } - }), - ...(resource.stickySession - ? { - sticky: { - cookie: { - name: "p_sticky", // TODO: make this configurable via config.yml like other cookies - secure: resource.ssl, - httpOnly: true - } - } - } - : {}) - } - }; - - // Add the serversTransport if TLS server name is provided - if (resource.tlsServerName) { - if (!config_output.http.serversTransports) { - config_output.http.serversTransports = {}; - } - config_output.http.serversTransports![transportName] = { - serverName: resource.tlsServerName, - //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings - // if defined in the static config and here. if not set, self-signed certs won't work - insecureSkipVerify: true - }; - config_output.http.services![ - serviceName - ].loadBalancer.serversTransport = transportName; - } - - // Add the host header middleware - if (resource.setHostHeader) { - if (!config_output.http.middlewares) { - config_output.http.middlewares = {}; - } - config_output.http.middlewares[hostHeaderMiddlewareName] = { - headers: { - customRequestHeaders: { - Host: resource.setHostHeader - } - } - }; - if (!config_output.http.routers![routerName].middlewares) { - config_output.http.routers![routerName].middlewares = - []; - } - config_output.http.routers![routerName].middlewares = [ - ...config_output.http.routers![routerName].middlewares, - hostHeaderMiddlewareName - ]; - } - } else { - // Non-HTTP (TCP/UDP) configuration - if (!resource.enableProxy) { - continue; - } - - const protocol = resource.protocol.toLowerCase(); - const port = resource.proxyPort; - - if (!port) { - continue; - } - - if (!config_output[protocol]) { - config_output[protocol] = { - routers: {}, - services: {} - }; - } - - config_output[protocol].routers[routerName] = { - entryPoints: [`${protocol}-${port}`], - service: serviceName, - ...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {}) - }; - - config_output[protocol].services[serviceName] = { - loadBalancer: { - servers: targets - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - if (!target.ip || !target.port) { - return false; - } - } else if (target.site.type === "newt") { - if (!target.internalPort || !target.site.subnet) { - return false; - } - } - return true; - }) - .map((target: TargetWithSite) => { - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - return { - address: `${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { - const ip = target.site.subnet!.split("/")[0]; - return { - address: `${ip}:${target.internalPort}` - }; - } - }), - ...(resource.stickySession - ? { - sticky: { - ipStrategy: { - depth: 0, - sourcePort: true - } - } - } - : {}) - } ->>>>>>> dev }; } From d548563e652fa92110d2c2fa40f03a95247af088 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 16 Aug 2025 14:54:16 -0700 Subject: [PATCH 137/219] Export the right thing --- server/lib/exitNodes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/exitNodes/index.ts b/server/lib/exitNodes/index.ts index b29bce93..caae390a 100644 --- a/server/lib/exitNodes/index.ts +++ b/server/lib/exitNodes/index.ts @@ -1 +1 @@ -export * from "./privateExitNodes"; \ No newline at end of file +export * from "./exitNodes"; \ No newline at end of file From d771317e3f8f47bec43e6ff417a5ef6f4cfba7a7 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 16 Aug 2025 14:57:19 -0700 Subject: [PATCH 138/219] Fix traefik config --- server/routers/traefik/getTraefikConfig.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 7349df02..e25a48cf 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -7,16 +7,6 @@ import config from "@server/lib/config"; import { orgs, resources, sites, Target, targets } from "@server/db"; import { build } from "@server/build"; -// Extended Target interface that includes site information -interface TargetWithSite extends Target { - site: { - siteId: number; - type: string; - subnet: string | null; - exitNodeId: number | null; - }; -} - let currentExitNodeId: number; const redirectHttpsMiddlewareName = "redirect-to-https"; const badgerMiddlewareName = "badger"; @@ -134,7 +124,7 @@ export async function getTraefikConfig(exitNodeId: number): Promise { or( eq(sites.exitNodeId, currentExitNodeId), isNull(sites.exitNodeId) - ) + ), ) ); @@ -201,7 +191,7 @@ export async function getTraefikConfig(exitNodeId: number): Promise { }; for (const resource of allResources) { - const targets = resource.targets as Target[]; + const targets = resource.targets; const site = resource.site; const routerName = `${resource.resourceId}-router`; From 609435328ed65cfbdc5c3bf230382aabb03063f9 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 16 Aug 2025 16:42:34 -0700 Subject: [PATCH 139/219] Smoothing over initial connection issues --- server/lib/exitNodes/index.ts | 3 +- server/lib/exitNodes/shared.ts | 30 +++++++++++++++ server/lib/remoteTraefikConfig.ts | 61 +++++++++++++++--------------- server/lib/tokenManager.ts | 27 ++++++------- server/routers/gerbil/getConfig.ts | 33 +++------------- 5 files changed, 80 insertions(+), 74 deletions(-) create mode 100644 server/lib/exitNodes/shared.ts diff --git a/server/lib/exitNodes/index.ts b/server/lib/exitNodes/index.ts index caae390a..8889bc35 100644 --- a/server/lib/exitNodes/index.ts +++ b/server/lib/exitNodes/index.ts @@ -1 +1,2 @@ -export * from "./exitNodes"; \ No newline at end of file +export * from "./exitNodes"; +export * from "./shared"; \ No newline at end of file diff --git a/server/lib/exitNodes/shared.ts b/server/lib/exitNodes/shared.ts new file mode 100644 index 00000000..c06f1d05 --- /dev/null +++ b/server/lib/exitNodes/shared.ts @@ -0,0 +1,30 @@ +import { db, exitNodes } from "@server/db"; +import config from "@server/lib/config"; +import { findNextAvailableCidr } from "@server/lib/ip"; + +export async function getNextAvailableSubnet(): Promise { + // Get all existing subnets from routes table + const existingAddresses = await db + .select({ + address: exitNodes.address + }) + .from(exitNodes); + + const addresses = existingAddresses.map((a) => a.address); + let subnet = findNextAvailableCidr( + addresses, + config.getRawConfig().gerbil.block_size, + config.getRawConfig().gerbil.subnet_group + ); + if (!subnet) { + throw new Error("No available subnets remaining in space"); + } + + // replace the last octet with 1 + subnet = + subnet.split(".").slice(0, 3).join(".") + + ".1" + + "/" + + subnet.split("/")[1]; + return subnet; +} \ No newline at end of file diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index e192ab67..d6289dea 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -155,14 +155,11 @@ export class TraefikConfigManager { method: error.config?.method }); } else { - logger.error( - "Error updating local SNI:", - error - ); + logger.error("Error updating local SNI:", error); } } } else { - logger.error("No exit node found"); + logger.error("No exit node found. Has gerbil registered yet?"); } } catch (err) { logger.error("Failed to post domains to SNI proxy:", err); @@ -213,35 +210,39 @@ export class TraefikConfigManager { } } - const badgerMiddlewareName = "badger"; - traefikConfig.http.middlewares[badgerMiddlewareName] = { - plugin: { - [badgerMiddlewareName]: { - apiBaseUrl: new URL( - "/api/v1", - `http://${ - config.getRawConfig().server.internal_hostname - }:${config.getRawConfig().server.internal_port}` - ).href, - userSessionCookieName: - config.getRawConfig().server.session_cookie_name, - - // deprecated - accessTokenQueryParam: - config.getRawConfig().server - .resource_access_token_param, - - resourceSessionRequestParam: - config.getRawConfig().server - .resource_session_request_param - } - } - }; - // logger.debug( // `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` // ); + const badgerMiddlewareName = "badger"; + if (traefikConfig?.http?.middlewares) { + traefikConfig.http.middlewares[badgerMiddlewareName] = { + plugin: { + [badgerMiddlewareName]: { + apiBaseUrl: new URL( + "/api/v1", + `http://${ + config.getRawConfig().server + .internal_hostname + }:${config.getRawConfig().server.internal_port}` + ).href, + userSessionCookieName: + config.getRawConfig().server + .session_cookie_name, + + // deprecated + accessTokenQueryParam: + config.getRawConfig().server + .resource_access_token_param, + + resourceSessionRequestParam: + config.getRawConfig().server + .resource_session_request_param + } + } + }; + } + return { domains, traefikConfig }; } catch (error) { // pull data out of the axios error to log diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts index 8abfd969..99330a3c 100644 --- a/server/lib/tokenManager.ts +++ b/server/lib/tokenManager.ts @@ -150,25 +150,20 @@ export class TokenManager { this.token = response.data.data.token; logger.debug("Token refreshed successfully"); } catch (error) { - logger.error("Failed to refresh token:", error); - if (axios.isAxiosError(error)) { - if (error.response) { - throw new Error( - `Failed to get token with status code: ${error.response.status}` - ); - } else if (error.request) { - throw new Error( - "Failed to request new token: No response received" - ); - } else { - throw new Error( - `Failed to request new token: ${error.message}` - ); - } + logger.error("Error updating proxy mapping:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); } else { - throw new Error(`Failed to get token: ${error}`); + logger.error("Error updating proxy mapping:", error); } + + throw new Error("Failed to refresh token"); } finally { this.isRefreshing = false; } diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index d8f4c56e..7cf69245 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -12,6 +12,7 @@ import { findNextAvailableCidr } from "@server/lib/ip"; import { fromError } from "zod-validation-error"; import { getAllowedIps } from "../target/helpers"; import { proxyToRemote } from "@server/lib/remoteProxy"; +import { getNextAvailableSubnet } from "@server/lib/exitNodes"; // Define Zod schema for request validation const getConfigSchema = z.object({ publicKey: z.string(), @@ -104,6 +105,11 @@ export async function getConfig( // STOP HERE IN HYBRID MODE if (config.isHybridMode()) { + req.body = { + ...req.body, + endpoint: exitNode[0].endpoint, + listenPort: exitNode[0].listenPort + } return proxyToRemote(req, res, next, "hybrid/gerbil/get-config"); } @@ -164,33 +170,6 @@ export async function generateGerbilConfig(exitNode: ExitNode) { return configResponse; } -async function getNextAvailableSubnet(): Promise { - // Get all existing subnets from routes table - const existingAddresses = await db - .select({ - address: exitNodes.address - }) - .from(exitNodes); - - const addresses = existingAddresses.map((a) => a.address); - let subnet = findNextAvailableCidr( - addresses, - config.getRawConfig().gerbil.block_size, - config.getRawConfig().gerbil.subnet_group - ); - if (!subnet) { - throw new Error("No available subnets remaining in space"); - } - - // replace the last octet with 1 - subnet = - subnet.split(".").slice(0, 3).join(".") + - ".1" + - "/" + - subnet.split("/")[1]; - return subnet; -} - async function getNextAvailablePort(): Promise { // Get all existing ports from exitNodes table const existingPorts = await db From 7ca507b1ceee1b7f5582ac9ecc76fec382b9d372 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 16 Aug 2025 17:16:19 -0700 Subject: [PATCH 140/219] Fixing traefik problems --- server/lib/remoteTraefikConfig.ts | 6 +-- server/routers/traefik/getTraefikConfig.ts | 58 +++++++++++++--------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index d6289dea..a31aee29 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -210,9 +210,9 @@ export class TraefikConfigManager { } } - // logger.debug( - // `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` - // ); + logger.debug( + `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` + ); const badgerMiddlewareName = "badger"; if (traefikConfig?.http?.middlewares) { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index e25a48cf..441b4328 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -81,6 +81,16 @@ export async function traefikConfigProvider( } export async function getTraefikConfig(exitNodeId: number): Promise { + // Define extended target type with site information + type TargetWithSite = Target & { + site: { + siteId: number; + type: string; + subnet: string | null; + exitNodeId: number | null; + }; + }; + // Get all resources with related data const allResources = await db.transaction(async (tx) => { // Get resources with their targets and sites in a single optimized query @@ -122,9 +132,10 @@ export async function getTraefikConfig(exitNodeId: number): Promise { eq(targets.enabled, true), eq(resources.enabled, true), or( - eq(sites.exitNodeId, currentExitNodeId), + eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId) ), + ne(targetHealthCheck.hcHealth, "unhealthy") ) ); @@ -192,7 +203,6 @@ export async function getTraefikConfig(exitNodeId: number): Promise { for (const resource of allResources) { const targets = resource.targets; - const site = resource.site; const routerName = `${resource.resourceId}-router`; const serviceName = `${resource.resourceId}-service`; @@ -295,14 +305,14 @@ export async function getTraefikConfig(exitNodeId: number): Promise { config_output.http.services![serviceName] = { loadBalancer: { - servers: targets - .filter((target: Target) => { + servers: (targets as TargetWithSite[]) + .filter((target: TargetWithSite) => { if (!target.enabled) { return false; } if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { if ( !target.ip || @@ -311,27 +321,27 @@ export async function getTraefikConfig(exitNodeId: number): Promise { ) { return false; } - } else if (site.type === "newt") { + } else if (target.site.type === "newt") { if ( !target.internalPort || !target.method || - !site.subnet + !target.site.subnet ) { return false; } } return true; }) - .map((target: Target) => { + .map((target: TargetWithSite) => { if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { return { url: `${target.method}://${target.ip}:${target.port}` }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; + } else if (target.site.type === "newt") { + const ip = target.site.subnet!.split("/")[0]; return { url: `${target.method}://${ip}:${target.internalPort}` }; @@ -415,35 +425,35 @@ export async function getTraefikConfig(exitNodeId: number): Promise { config_output[protocol].services[serviceName] = { loadBalancer: { - servers: targets - .filter((target: Target) => { + servers: (targets as TargetWithSite[]) + .filter((target: TargetWithSite) => { if (!target.enabled) { return false; } if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { if (!target.ip || !target.port) { return false; } - } else if (site.type === "newt") { - if (!target.internalPort || !site.subnet) { + } else if (target.site.type === "newt") { + if (!target.internalPort || !target.site.subnet) { return false; } } return true; }) - .map((target: Target) => { + .map((target: TargetWithSite) => { if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { return { address: `${target.ip}:${target.port}` }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; + } else if (target.site.type === "newt") { + const ip = target.site.subnet!.split("/")[0]; return { address: `${ip}:${target.internalPort}` }; From 83a696f74399620d73c6988e618bb2485f2e6a78 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 16 Aug 2025 17:29:27 -0700 Subject: [PATCH 141/219] Make traefik config wor --- server/lib/remoteTraefikConfig.ts | 6 +++--- server/routers/traefik/getTraefikConfig.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index a31aee29..d6289dea 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -210,9 +210,9 @@ export class TraefikConfigManager { } } - logger.debug( - `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` - ); + // logger.debug( + // `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` + // ); const badgerMiddlewareName = "badger"; if (traefikConfig?.http?.middlewares) { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 441b4328..d93f7ac0 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -135,7 +135,6 @@ export async function getTraefikConfig(exitNodeId: number): Promise { eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId) ), - ne(targetHealthCheck.hcHealth, "unhealthy") ) ); From 8355d3664e067fa63caa501bd1b618a771a33a4c Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 16 Aug 2025 17:53:33 -0700 Subject: [PATCH 142/219] Retry the token request --- server/lib/tokenManager.ts | 93 ++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts index 99330a3c..e6a03067 100644 --- a/server/lib/tokenManager.ts +++ b/server/lib/tokenManager.ts @@ -33,34 +33,96 @@ export class TokenManager { private refreshInterval: NodeJS.Timeout | null = null; private isRefreshing: boolean = false; private refreshIntervalMs: number; + private retryInterval: NodeJS.Timeout | null = null; + private retryIntervalMs: number; + private tokenAvailablePromise: Promise | null = null; + private tokenAvailableResolve: (() => void) | null = null; - constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000) { - // Default to 24 hours + constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) { + // Default to 24 hours for refresh, 5 seconds for retry this.refreshIntervalMs = refreshIntervalMs; + this.retryIntervalMs = retryIntervalMs; + this.setupTokenAvailablePromise(); } /** - * Start the token manager - gets initial token and sets up refresh interval + * Set up promise that resolves when token becomes available */ - async start(): Promise { - try { - await this.refreshToken(); - this.setupRefreshInterval(); - logger.info("Token manager started successfully"); - } catch (error) { - logger.error("Failed to start token manager:", error); - throw error; + private setupTokenAvailablePromise(): void { + this.tokenAvailablePromise = new Promise((resolve) => { + this.tokenAvailableResolve = resolve; + }); + } + + /** + * Resolve the token available promise + */ + private resolveTokenAvailable(): void { + if (this.tokenAvailableResolve) { + this.tokenAvailableResolve(); + this.tokenAvailableResolve = null; } } /** - * Stop the token manager and clear refresh interval + * Start the token manager - gets initial token and sets up refresh interval + * If initial token fetch fails, keeps retrying every few seconds until successful + */ + async start(): Promise { + logger.info("Starting token manager..."); + + try { + await this.refreshToken(); + this.setupRefreshInterval(); + this.resolveTokenAvailable(); + logger.info("Token manager started successfully"); + } catch (error) { + logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error); + this.setupRetryInterval(); + } + } + + /** + * Set up retry interval for initial token acquisition + */ + private setupRetryInterval(): void { + if (this.retryInterval) { + clearInterval(this.retryInterval); + } + + this.retryInterval = setInterval(async () => { + try { + logger.debug("Retrying initial token acquisition"); + await this.refreshToken(); + this.setupRefreshInterval(); + this.clearRetryInterval(); + this.resolveTokenAvailable(); + logger.info("Token manager started successfully after retry"); + } catch (error) { + logger.debug("Token acquisition retry failed, will try again"); + } + }, this.retryIntervalMs); + } + + /** + * Clear retry interval + */ + private clearRetryInterval(): void { + if (this.retryInterval) { + clearInterval(this.retryInterval); + this.retryInterval = null; + } + } + + /** + * Stop the token manager and clear all intervals */ stop(): void { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } + this.clearRetryInterval(); logger.info("Token manager stopped"); } @@ -70,12 +132,17 @@ export class TokenManager { // TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT async getToken(): Promise { + // If we don't have a token yet, wait for it to become available + if (!this.token && this.tokenAvailablePromise) { + await this.tokenAvailablePromise; + } + if (!this.token) { if (this.isRefreshing) { // Wait for current refresh to complete await this.waitForRefresh(); } else { - await this.refreshToken(); + throw new Error("No valid token available"); } } From 3b8d1f40a76fc6f131a06f977baaa4d4a96b5665 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 11:23:43 -0700 Subject: [PATCH 143/219] Include get hostname, filter sites fix gerbil conf --- .gitignore | 4 ++ docker-compose.example.yml | 3 +- install/config/docker-compose.yml | 3 +- server/lib/exitNodes/exitNodes.ts | 15 +++---- server/routers/gerbil/getResolvedHostname.ts | 46 ++++++++++++++++++++ server/routers/gerbil/index.ts | 3 +- server/routers/internal.ts | 5 +++ server/routers/traefik/getTraefikConfig.ts | 5 ++- 8 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 server/routers/gerbil/getResolvedHostname.ts diff --git a/.gitignore b/.gitignore index 2f1749ef..78ce996b 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,10 @@ bin .secrets test_event.json .idea/ +public/branding server/db/index.ts +config/openapi.yaml +server/build.ts +postgres/ dynamic/ certificates/ diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 703c47c6..28097f32 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -22,8 +22,7 @@ services: command: - --reachableAt=http://gerbil:3003 - --generateAndSaveKeyTo=/var/config/key - - --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config - - --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth + - --remoteConfig=http://pangolin:3001/api/v1/ volumes: - ./config/:/var/config cap_add: diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 70a4602f..44af4199 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -22,8 +22,7 @@ services: command: - --reachableAt=http://gerbil:3003 - --generateAndSaveKeyTo=/var/config/key - - --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config - - --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth + - --remoteConfig=http://pangolin:3001/api/v1/ volumes: - ./config/:/var/config cap_add: diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts index f607371d..06539bb0 100644 --- a/server/lib/exitNodes/exitNodes.ts +++ b/server/lib/exitNodes/exitNodes.ts @@ -1,16 +1,16 @@ import { db, exitNodes } from "@server/db"; import logger from "@server/logger"; import { ExitNodePingResult } from "@server/routers/newt"; -import { eq, and, or } from "drizzle-orm"; +import { eq } from "drizzle-orm"; export async function verifyExitNodeOrgAccess( exitNodeId: number, orgId: string ) { const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, exitNodeId)); + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, exitNodeId)); // For any other type, deny access return { hasAccess: true, exitNode }; @@ -30,7 +30,7 @@ export async function listExitNodes(orgId: string, filterOnline = false) { maxConnections: exitNodes.maxConnections, online: exitNodes.online, lastPing: exitNodes.lastPing, - type: exitNodes.type, + type: exitNodes.type }) .from(exitNodes); @@ -54,9 +54,6 @@ export function selectBestExitNode( return pingResults[0]; } -export async function checkExitNodeOrg( - exitNodeId: number, - orgId: string -) { +export async function checkExitNodeOrg(exitNodeId: number, orgId: string) { return false; } \ No newline at end of file diff --git a/server/routers/gerbil/getResolvedHostname.ts b/server/routers/gerbil/getResolvedHostname.ts new file mode 100644 index 00000000..da2ab39a --- /dev/null +++ b/server/routers/gerbil/getResolvedHostname.ts @@ -0,0 +1,46 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +// Define Zod schema for request validation +const getResolvedHostnameSchema = z.object({ + hostname: z.string(), + publicKey: z.string() +}); + +export async function getResolvedHostname( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + // Validate request parameters + const parsedParams = getResolvedHostnameSchema.safeParse( + req.body + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + // return the endpoints + return res.status(HttpCode.OK).send({ + endpoints: [] // ALWAYS ROUTE LOCALLY + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred..." + ) + ); + } +} diff --git a/server/routers/gerbil/index.ts b/server/routers/gerbil/index.ts index 4a4f3b60..bff57d05 100644 --- a/server/routers/gerbil/index.ts +++ b/server/routers/gerbil/index.ts @@ -1,4 +1,5 @@ export * from "./getConfig"; export * from "./receiveBandwidth"; export * from "./updateHolePunch"; -export * from "./getAllRelays"; \ No newline at end of file +export * from "./getAllRelays"; +export * from "./getResolvedHostname"; \ No newline at end of file diff --git a/server/routers/internal.ts b/server/routers/internal.ts index d19355b7..805e284f 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -66,6 +66,10 @@ if (config.isHybridMode()) { proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays") ); + gerbilRouter.post("/get-resolved-hostname", (req, res, next) => + proxyToRemote(req, res, next, `hybrid/gerbil/get-resolved-hostname`) + ); + // GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER // SO IT CAN REGISTER THE LOCAL EXIT NODE } else { @@ -73,6 +77,7 @@ if (config.isHybridMode()) { gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); + gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname); } // WE HANDLE THE PROXY INSIDE OF THIS FUNCTION diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index d93f7ac0..422f9739 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -45,7 +45,7 @@ export async function traefikConfigProvider( } } - let traefikConfig = await getTraefikConfig(currentExitNodeId); + let traefikConfig = await getTraefikConfig(currentExitNodeId, ["newt", "local", "wireguard"]); traefikConfig.http.middlewares[badgerMiddlewareName] = { plugin: { @@ -80,7 +80,7 @@ export async function traefikConfigProvider( } } -export async function getTraefikConfig(exitNodeId: number): Promise { +export async function getTraefikConfig(exitNodeId: number, siteTypes: string[]): Promise { // Define extended target type with site information type TargetWithSite = Target & { site: { @@ -135,6 +135,7 @@ export async function getTraefikConfig(exitNodeId: number): Promise { eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId) ), + inArray(sites.type, siteTypes), ) ); From af2088df4e79a91bd65770acdadd4f767b87a0d5 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 18:01:36 -0700 Subject: [PATCH 144/219] Control which types of sites work and tell user --- messages/en-US.json | 2 ++ server/lib/readConfigFile.ts | 3 ++- server/routers/traefik/getTraefikConfig.ts | 2 +- src/app/[orgId]/settings/sites/create/page.tsx | 5 +++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 6f80cbe9..7f00e40d 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", "siteWg": "Basic WireGuard", "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Local resources only. No tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "See All Sites", "siteTunnelDescription": "Determine how you want to connect to your site", "siteNewtCredentials": "Newt Credentials", diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index f52e1f99..23db4e52 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -154,7 +154,8 @@ export const configSchema = z .string() .optional() .default("./dynamic/router_config.yml"), - staticDomains: z.array(z.string()).optional().default([]) + static_domains: z.array(z.string()).optional().default([]), + site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]) }) .optional() .default({}), diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 422f9739..4ec2908c 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -45,7 +45,7 @@ export async function traefikConfigProvider( } } - let traefikConfig = await getTraefikConfig(currentExitNodeId, ["newt", "local", "wireguard"]); + let traefikConfig = await getTraefikConfig(currentExitNodeId, config.getRawConfig().traefik.site_types); traefikConfig.http.middlewares[badgerMiddlewareName] = { plugin: { diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 87524d1c..26cba229 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -59,6 +59,7 @@ import { useParams, useRouter } from "next/navigation"; import { QRCodeCanvas } from "qrcode.react"; import { useTranslations } from "next-intl"; +import { build } from "@server/build"; type SiteType = "newt" | "wireguard" | "local"; @@ -142,7 +143,7 @@ export default function Page() { { id: "wireguard" as SiteType, title: t("siteWg"), - description: t("siteWgDescription"), + description: build == "saas" ? t("siteWgDescriptionSaas") : t("siteWgDescription"), disabled: true } ]), @@ -152,7 +153,7 @@ export default function Page() { { id: "local" as SiteType, title: t("local"), - description: t("siteLocalDescription") + description: build == "saas" ? t("siteLocalDescriptionSaas") : t("siteLocalDescription") } ]) ]); From b805daec5174855afb274cf3ca63ac3df64054f2 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 18:18:26 -0700 Subject: [PATCH 145/219] Move to build arg --- Dockerfile | 46 ++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 12 ++++++------ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5da62593 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM node:22-alpine AS builder + +WORKDIR /app + +ARG BUILD=oss +ARG DATABASE=sqlite + +# COPY package.json package-lock.json ./ +COPY package*.json ./ +RUN npm ci + +COPY . . + +RUN echo 'export * from "./\"$DATABASE\";' > server/db/index.ts + +RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts + +RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init + +RUN npm run build:sqlite +RUN npm run build:cli + +FROM node:22-alpine AS runner + +WORKDIR /app + +# Curl used for the health checks +RUN apk add --no-cache curl + +# COPY package.json package-lock.json ./ +COPY package*.json ./ +RUN npm ci --omit=dev && npm cache clean --force + +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/init ./dist/init + +COPY ./cli/wrapper.sh /usr/local/bin/pangctl +RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs + +COPY server/db/names.json ./dist/names.json + +COPY public ./public + +CMD ["npm", "run", "start:sqlite"] diff --git a/Makefile b/Makefile index 0e0394b4..de67a5f2 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ build-release: echo "Error: tag is required. Usage: make build-release tag="; \ exit 1; \ fi - docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile.sqlite --push . - docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile.sqlite --push . - docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push . - docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push . + docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest --push . + docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) --push . + docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest --push . + docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) --push . build-arm: docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest . @@ -17,10 +17,10 @@ build-x86: docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . build-sqlite: - docker build -t fosrl/pangolin:latest -f Dockerfile.sqlite . + docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest . build-pg: - docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg . + docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest . test: docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest From c1d75d32c273d5757ef8363918be6a1956db4394 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 18:19:33 -0700 Subject: [PATCH 146/219] Remove old docker files --- Dockerfile.pg | 41 ----------------------------------------- Dockerfile.sqlite | 41 ----------------------------------------- 2 files changed, 82 deletions(-) delete mode 100644 Dockerfile.pg delete mode 100644 Dockerfile.sqlite diff --git a/Dockerfile.pg b/Dockerfile.pg deleted file mode 100644 index 8e45068d..00000000 --- a/Dockerfile.pg +++ /dev/null @@ -1,41 +0,0 @@ -FROM node:22-alpine AS builder - -WORKDIR /app - -# COPY package.json package-lock.json ./ -COPY package*.json ./ -RUN npm ci - -COPY . . - -RUN echo 'export * from "./pg";' > server/db/index.ts - -RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init - -RUN npm run build:pg -RUN npm run build:cli - -FROM node:22-alpine AS runner - -WORKDIR /app - -# Curl used for the health checks -RUN apk add --no-cache curl - -# COPY package.json package-lock.json ./ -COPY package*.json ./ -RUN npm ci --omit=dev && npm cache clean --force - -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/init ./dist/init - -COPY ./cli/wrapper.sh /usr/local/bin/pangctl -RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs - -COPY server/db/names.json ./dist/names.json - -COPY public ./public - -CMD ["npm", "run", "start:pg"] diff --git a/Dockerfile.sqlite b/Dockerfile.sqlite deleted file mode 100644 index 6a24a4af..00000000 --- a/Dockerfile.sqlite +++ /dev/null @@ -1,41 +0,0 @@ -FROM node:22-alpine AS builder - -WORKDIR /app - -# COPY package.json package-lock.json ./ -COPY package*.json ./ -RUN npm ci - -COPY . . - -RUN echo 'export * from "./sqlite";' > server/db/index.ts - -RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init - -RUN npm run build:sqlite -RUN npm run build:cli - -FROM node:22-alpine AS runner - -WORKDIR /app - -# Curl used for the health checks -RUN apk add --no-cache curl - -# COPY package.json package-lock.json ./ -COPY package*.json ./ -RUN npm ci --omit=dev && npm cache clean --force - -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/init ./dist/init - -COPY ./cli/wrapper.sh /usr/local/bin/pangctl -RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs - -COPY server/db/names.json ./dist/names.json - -COPY public ./public - -CMD ["npm", "run", "start:sqlite"] From c8bea4d7de245d9da55fa5ed686d9c246e97cc52 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 18:20:53 -0700 Subject: [PATCH 147/219] Finish adding arg --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5da62593..306705ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init -RUN npm run build:sqlite +RUN npm run build:$DATABASE RUN npm run build:cli FROM node:22-alpine AS runner @@ -43,4 +43,4 @@ COPY server/db/names.json ./dist/names.json COPY public ./public -CMD ["npm", "run", "start:sqlite"] +CMD ["npm", "run", "start:$DATABASE"] From 632333c49f11eb2bee82142b5d83cedd536bbef1 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 18:31:08 -0700 Subject: [PATCH 148/219] Fix build args again --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 306705ec..241666ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,11 @@ RUN npm ci COPY . . -RUN echo 'export * from "./\"$DATABASE\";' > server/db/index.ts +RUN echo "export * from ./\"$DATABASE\";" > server/db/index.ts RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts -RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init +RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi RUN npm run build:$DATABASE RUN npm run build:cli From 33a2ac402c7643e2c0727d71d3f134075fd2c764 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 18:36:23 -0700 Subject: [PATCH 149/219] Fix " --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 241666ed..f84cb36d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN npm ci COPY . . -RUN echo "export * from ./\"$DATABASE\";" > server/db/index.ts +RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts From 8c8a981452d358c410f014c2aa3cbd7ff4cc664b Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 20:18:10 -0700 Subject: [PATCH 150/219] Make more efficient the cert get --- server/lib/remoteTraefikConfig.ts | 232 +++++++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 7 deletions(-) diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/remoteTraefikConfig.ts index d6289dea..72e3492e 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/remoteTraefikConfig.ts @@ -12,6 +12,13 @@ export class TraefikConfigManager { private isRunning = false; private activeDomains = new Set(); private timeoutId: NodeJS.Timeout | null = null; + private lastCertificateFetch: Date | null = null; + private lastKnownDomains = new Set(); + private lastLocalCertificateState = new Map(); constructor() {} @@ -50,6 +57,10 @@ export class TraefikConfigManager { config.getRawConfig().traefik.certificates_path ); + // Initialize local certificate state + this.lastLocalCertificateState = await this.scanLocalCertificateState(); + logger.info(`Found ${this.lastLocalCertificateState.size} existing certificate directories`); + // Run initial check await this.HandleTraefikConfig(); @@ -80,6 +91,113 @@ export class TraefikConfigManager { logger.info("Certificate monitor stopped"); } + /** + * Scan local certificate directories to build current state + */ + private async scanLocalCertificateState(): Promise> { + const state = new Map(); + const certsPath = config.getRawConfig().traefik.certificates_path; + + try { + if (!fs.existsSync(certsPath)) { + return state; + } + + const certDirs = fs.readdirSync(certsPath, { withFileTypes: true }); + + for (const dirent of certDirs) { + if (!dirent.isDirectory()) continue; + + const domain = dirent.name; + const domainDir = path.join(certsPath, domain); + const certPath = path.join(domainDir, "cert.pem"); + const keyPath = path.join(domainDir, "key.pem"); + const lastUpdatePath = path.join(domainDir, ".last_update"); + + const certExists = await this.fileExists(certPath); + const keyExists = await this.fileExists(keyPath); + const lastUpdateExists = await this.fileExists(lastUpdatePath); + + let lastModified: Date | null = null; + let expiresAt: Date | null = null; + + if (lastUpdateExists) { + try { + const lastUpdateStr = fs.readFileSync(lastUpdatePath, "utf8").trim(); + lastModified = new Date(lastUpdateStr); + } catch { + // If we can't read the last update, fall back to file stats + try { + const stats = fs.statSync(certPath); + lastModified = stats.mtime; + } catch { + lastModified = null; + } + } + } + + state.set(domain, { + exists: certExists && keyExists, + lastModified, + expiresAt + }); + } + } catch (error) { + logger.error("Error scanning local certificate state:", error); + } + + return state; + } + + /** + * Check if we need to fetch certificates from remote + */ + private shouldFetchCertificates(currentDomains: Set): boolean { + // Always fetch on first run + if (!this.lastCertificateFetch) { + return true; + } + + // Fetch if it's been more than 24 hours (for renewals) + const dayInMs = 24 * 60 * 60 * 1000; + const timeSinceLastFetch = Date.now() - this.lastCertificateFetch.getTime(); + if (timeSinceLastFetch > dayInMs) { + logger.info("Fetching certificates due to 24-hour renewal check"); + return true; + } + + // Fetch if domains have changed + if (this.lastKnownDomains.size !== currentDomains.size || + !Array.from(this.lastKnownDomains).every(domain => currentDomains.has(domain))) { + logger.info("Fetching certificates due to domain changes"); + return true; + } + + // Check if any local certificates are missing or appear to be outdated + for (const domain of currentDomains) { + const localState = this.lastLocalCertificateState.get(domain); + if (!localState || !localState.exists) { + logger.info(`Fetching certificates due to missing local cert for ${domain}`); + return true; + } + + // Check if certificate is expiring soon (within 30 days) + if (localState.expiresAt) { + const daysUntilExpiry = (localState.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24); + if (daysUntilExpiry < 30) { + logger.info(`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)`); + return true; + } + } + } + + return false; + } + /** * Main monitoring logic */ @@ -115,14 +233,37 @@ export class TraefikConfigManager { this.lastActiveDomains = new Set(domains); } - // Get valid certificates for active domains - const validCertificates = - await this.getValidCertificatesForDomains(domains); + // Scan current local certificate state + this.lastLocalCertificateState = await this.scanLocalCertificateState(); - // logger.debug(`Valid certs array: ${JSON.stringify(validCertificates)}`); + // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) + let validCertificates: Array<{ + id: number; + domain: string; + certFile: string | null; + keyFile: string | null; + expiresAt: Date | null; + updatedAt?: Date | null; + }> = []; - // Download and decrypt new certificates - await this.processValidCertificates(validCertificates); + if (this.shouldFetchCertificates(domains)) { + // Get valid certificates for active domains + validCertificates = await this.getValidCertificatesForDomains(domains); + this.lastCertificateFetch = new Date(); + this.lastKnownDomains = new Set(domains); + + logger.info(`Fetched ${validCertificates.length} certificates from remote`); + + // Download and decrypt new certificates + await this.processValidCertificates(validCertificates); + } else { + const timeSinceLastFetch = this.lastCertificateFetch ? + Math.round((Date.now() - this.lastCertificateFetch.getTime()) / (1000 * 60)) : 0; + logger.debug(`Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`); + + // Still need to ensure config is up to date with existing certificates + await this.updateDynamicConfigFromLocalCerts(domains); + } // Clean up certificates for domains no longer in use await this.cleanupUnusedCertificates(domains); @@ -301,6 +442,59 @@ export class TraefikConfigManager { } } + /** + * Update dynamic config from existing local certificates without fetching from remote + */ + private async updateDynamicConfigFromLocalCerts(domains: Set): Promise { + const dynamicConfigPath = config.getRawConfig().traefik.dynamic_cert_config_path; + + // Load existing dynamic config if it exists, otherwise initialize + let dynamicConfig: any = { tls: { certificates: [] } }; + if (fs.existsSync(dynamicConfigPath)) { + try { + const fileContent = fs.readFileSync(dynamicConfigPath, "utf8"); + dynamicConfig = yaml.load(fileContent) || dynamicConfig; + if (!dynamicConfig.tls) dynamicConfig.tls = { certificates: [] }; + if (!Array.isArray(dynamicConfig.tls.certificates)) { + dynamicConfig.tls.certificates = []; + } + } catch (err) { + logger.error("Failed to load existing dynamic config:", err); + } + } + + // Keep a copy of the original config for comparison + const originalConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); + + // Clear existing certificates and rebuild from local state + dynamicConfig.tls.certificates = []; + + for (const domain of domains) { + const localState = this.lastLocalCertificateState.get(domain); + if (localState && localState.exists) { + const domainDir = path.join( + config.getRawConfig().traefik.certificates_path, + domain + ); + const certPath = path.join(domainDir, "cert.pem"); + const keyPath = path.join(domainDir, "key.pem"); + + const certEntry = { + certFile: `/var/${certPath}`, + keyFile: `/var/${keyPath}` + }; + dynamicConfig.tls.certificates.push(certEntry); + } + } + + // Only write the config if it has changed + const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); + if (newConfigYaml !== originalConfigYaml) { + fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); + logger.info("Dynamic cert config updated from local certificates"); + } + } + /** * Get valid certificates for the specified domains */ @@ -446,6 +640,13 @@ export class TraefikConfigManager { logger.info( `Certificate updated for domain: ${cert.domain}` ); + + // Update local state tracking + this.lastLocalCertificateState.set(cert.domain, { + exists: true, + lastModified: new Date(), + expiresAt: cert.expiresAt + }); } // Always ensure the config entry exists and is up to date @@ -591,6 +792,9 @@ export class TraefikConfigManager { ); fs.rmSync(domainDir, { recursive: true, force: true }); + // Remove from local state tracking + this.lastLocalCertificateState.delete(dirName); + // Remove from dynamic config const certFilePath = `/var/${path.join( domainDir, @@ -657,6 +861,16 @@ export class TraefikConfigManager { } } + /** + * Force a certificate refresh regardless of cache state + */ + public async forceCertificateRefresh(): Promise { + logger.info("Forcing certificate refresh"); + this.lastCertificateFetch = null; + this.lastKnownDomains = new Set(); + await this.HandleTraefikConfig(); + } + /** * Get current status */ @@ -664,12 +878,16 @@ export class TraefikConfigManager { isRunning: boolean; activeDomains: string[]; monitorInterval: number; + lastCertificateFetch: Date | null; + localCertificateCount: number; } { return { isRunning: this.isRunning, activeDomains: Array.from(this.activeDomains), monitorInterval: - config.getRawConfig().traefik.monitor_interval || 5000 + config.getRawConfig().traefik.monitor_interval || 5000, + lastCertificateFetch: this.lastCertificateFetch, + localCertificateCount: this.lastLocalCertificateState.size }; } } From 36c0d9aba2997d4583d31f7efda6bf55a89bf9b2 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 17 Aug 2025 21:29:07 -0700 Subject: [PATCH 151/219] add hybrid splash --- messages/en-US.json | 1 + server/lib/config.ts | 4 +- server/lib/readConfigFile.ts | 1 + server/routers/external.ts | 10 +- src/app/admin/managed/page.tsx | 176 +++++++++++++++++++++++++++++++ src/app/navigation.tsx | 15 ++- src/components/LayoutSidebar.tsx | 32 +++++- src/lib/types/env.ts | 2 +- 8 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 src/app/admin/managed/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 7f00e40d..602754d8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -973,6 +973,7 @@ "logoutError": "Error logging out", "signingAs": "Signed in as", "serverAdmin": "Server Admin", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Enable Two-factor", "otpDisable": "Disable Two-factor", "logout": "Log Out", diff --git a/server/lib/config.ts b/server/lib/config.ts index 6b41df79..82932441 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -103,9 +103,7 @@ export class Config { private async checkKeyStatus() { const licenseStatus = await license.check(); - if ( - !licenseStatus.isHostLicensed - ) { + if (!licenseStatus.isHostLicensed) { this.checkSupporterKey(); } } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 23db4e52..8107385c 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -34,6 +34,7 @@ export const configSchema = z }), hybrid: z .object({ + name: z.string().optional(), id: z.string().optional(), secret: z.string().optional(), endpoint: z.string().optional(), diff --git a/server/routers/external.ts b/server/routers/external.ts index fd7fff50..95add3ed 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -15,6 +15,7 @@ import * as accessToken from "./accessToken"; import * as idp from "./idp"; import * as license from "./license"; import * as apiKeys from "./apiKeys"; +import * as hybrid from "./hybrid"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -951,7 +952,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email || req.ip}`, + keyGenerator: (req) => + `requestEmailVerificationCode:${req.body.email || req.ip}`, handler: (req, res, next) => { const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -972,7 +974,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `requestPasswordReset:${req.body.email || req.ip}`, + keyGenerator: (req) => + `requestPasswordReset:${req.body.email || req.ip}`, handler: (req, res, next) => { const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -1066,7 +1069,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Allow 5 security key registrations per 15 minutes - keyGenerator: (req) => `securityKeyRegister:${req.user?.userId || req.ip}`, + keyGenerator: (req) => + `securityKeyRegister:${req.user?.userId || req.ip}`, handler: (req, res, next) => { const message = `You can only register a security key ${5} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); diff --git a/src/app/admin/managed/page.tsx b/src/app/admin/managed/page.tsx new file mode 100644 index 00000000..cb25ba5d --- /dev/null +++ b/src/app/admin/managed/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionTitle as SectionTitle, + SettingsSectionBody, + SettingsSectionFooter +} from "@app/components/Settings"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { Alert } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + Shield, + Zap, + RefreshCw, + Activity, + Wrench, + CheckCircle, + ExternalLink +} from "lucide-react"; +import Link from "next/link"; + +export default async function ManagedPage() { + return ( + <> + + + + + +

+ Managed Self-Hosted Pangolin is a + deployment option designed for people who want + simplicity and extra reliability while still keeping + their data private and self-hosted. +

+

+ With this option, you still run your own Pangolin + node — your tunnels, SSL termination, and traffic + all stay on your server. The difference is that + management and monitoring are handled through our + cloud dashboard, which unlocks a number of benefits: +

+ +
+
+
+ +
+

+ Simpler operations +

+

+ No need to run your own mail server + or set up complex alerting. You'll + get health checks and downtime + alerts out of the box. +

+
+
+ +
+ +
+

+ Automatic updates +

+

+ The cloud dashboard evolves quickly, + so you get new features and bug + fixes without having to manually + pull new containers every time. +

+
+
+ +
+ +
+

+ Less maintenance +

+

+ No database migrations, backups, or + extra infrastructure to manage. We + handle that in the cloud. +

+
+
+
+ +
+
+ +
+

+ Cloud failover +

+

+ If your node goes down, your tunnels + can temporarily fail over to our + cloud points of presence until you + bring it back online. +

+
+
+
+ +
+

+ High availability (PoPs) +

+

+ You can also attach multiple nodes + to your account for redundancy and + better performance. +

+
+
+ +
+ +
+

+ Future enhancements +

+

+ We're planning to add more + analytics, alerting, and management + tools to make your deployment even + more robust. +

+
+
+
+
+ + + Read the docs to learn more about the Managed + Self-Hosted option in our{" "} + + documentation + + + . + +
+ + + + + +
+
+ + ); +} diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index b26b98ec..f77bf3a9 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -13,10 +13,12 @@ import { TicketCheck, User, Globe, // Added from 'dev' branch - MonitorUp // Added from 'dev' branch + MonitorUp, // Added from 'dev' branch + Zap } from "lucide-react"; -export type SidebarNavSection = { // Added from 'dev' branch +export type SidebarNavSection = { + // Added from 'dev' branch heading: string; items: SidebarNavItem[]; }; @@ -108,6 +110,15 @@ export const adminNavSections: SidebarNavSection[] = [ { heading: "Admin", items: [ + ...(build == "oss" + ? [ + { + title: "managedSelfhosted", + href: "/admin/managed", + icon: + } + ] + : []), { title: "sidebarAllUsers", href: "/admin/users", diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index d309c11f..cfc21144 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -6,7 +6,7 @@ import { OrgSelector } from "@app/components/OrgSelector"; import { cn } from "@app/lib/cn"; import { ListUserOrgsResponse } from "@server/routers/org"; import SupporterStatus from "@app/components/SupporterStatus"; -import { ExternalLink, Server, BookOpenText } from "lucide-react"; +import { ExternalLink, Server, BookOpenText, Zap } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useUserContext } from "@app/hooks/useUserContext"; @@ -20,6 +20,7 @@ import { TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { build } from "@server/build"; interface LayoutSidebarProps { orgId?: string; @@ -73,6 +74,35 @@ export function LayoutSidebar({
{!isAdminPage && user.serverAdmin && (
+ {build === "oss" && ( + + + + + {!isSidebarCollapsed && ( + {t("managedSelfhosted")} + )} + + )} + Date: Sun, 17 Aug 2025 21:44:28 -0700 Subject: [PATCH 152/219] Also allow local traefikConfig --- server/lib/remoteCertificates/certificates.ts | 78 ++++++ server/lib/remoteCertificates/index.ts | 1 + ...emoteTraefikConfig.ts => traefikConfig.ts} | 249 +++++++++--------- server/routers/traefik/getTraefikConfig.ts | 73 ++--- 4 files changed, 253 insertions(+), 148 deletions(-) create mode 100644 server/lib/remoteCertificates/certificates.ts create mode 100644 server/lib/remoteCertificates/index.ts rename server/lib/{remoteTraefikConfig.ts => traefikConfig.ts} (85%) diff --git a/server/lib/remoteCertificates/certificates.ts b/server/lib/remoteCertificates/certificates.ts new file mode 100644 index 00000000..f9d98e93 --- /dev/null +++ b/server/lib/remoteCertificates/certificates.ts @@ -0,0 +1,78 @@ +import axios from "axios"; +import { tokenManager } from "../tokenManager"; +import logger from "@server/logger"; +import config from "../config"; + +/** + * Get valid certificates for the specified domains + */ +export async function getValidCertificatesForDomainsHybrid(domains: Set): Promise< + Array<{ + id: number; + domain: string; + certFile: string | null; + keyFile: string | null; + expiresAt: Date | null; + updatedAt?: Date | null; + }> +> { + if (domains.size === 0) { + return []; + } + + const domainArray = Array.from(domains); + + try { + const response = await axios.get( + `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/certificates/domains`, + { + params: { + domains: domainArray + }, + headers: (await tokenManager.getAuthHeader()).headers + } + ); + + if (response.status !== 200) { + logger.error( + `Failed to fetch certificates for domains: ${response.status} ${response.statusText}`, + { responseData: response.data, domains: domainArray } + ); + return []; + } + + // logger.debug( + // `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains` + // ); + + return response.data.data; + } catch (error) { + // pull data out of the axios error to log + if (axios.isAxiosError(error)) { + logger.error("Error getting certificates:", { + message: error.message, + code: error.code, + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method + }); + } else { + logger.error("Error getting certificates:", error); + } + return []; + } +} + +export async function getValidCertificatesForDomains(domains: Set): Promise< + Array<{ + id: number; + domain: string; + certFile: string | null; + keyFile: string | null; + expiresAt: Date | null; + updatedAt?: Date | null; + }> +> { + return []; // stub +} \ No newline at end of file diff --git a/server/lib/remoteCertificates/index.ts b/server/lib/remoteCertificates/index.ts new file mode 100644 index 00000000..53051b6c --- /dev/null +++ b/server/lib/remoteCertificates/index.ts @@ -0,0 +1 @@ +export * from "./certificates"; \ No newline at end of file diff --git a/server/lib/remoteTraefikConfig.ts b/server/lib/traefikConfig.ts similarity index 85% rename from server/lib/remoteTraefikConfig.ts rename to server/lib/traefikConfig.ts index 72e3492e..d62c5f7f 100644 --- a/server/lib/remoteTraefikConfig.ts +++ b/server/lib/traefikConfig.ts @@ -5,7 +5,16 @@ import logger from "@server/logger"; import * as yaml from "js-yaml"; import axios from "axios"; import { db, exitNodes } from "@server/db"; +import { eq } from "drizzle-orm"; import { tokenManager } from "./tokenManager"; +import { + getCurrentExitNodeId, + getTraefikConfig +} from "@server/routers/traefik"; +import { + getValidCertificatesForDomains, + getValidCertificatesForDomainsHybrid +} from "./remoteCertificates"; export class TraefikConfigManager { private intervalId: NodeJS.Timeout | null = null; @@ -14,11 +23,14 @@ export class TraefikConfigManager { private timeoutId: NodeJS.Timeout | null = null; private lastCertificateFetch: Date | null = null; private lastKnownDomains = new Set(); - private lastLocalCertificateState = new Map(); + private lastLocalCertificateState = new Map< + string, + { + exists: boolean; + lastModified: Date | null; + expiresAt: Date | null; + } + >(); constructor() {} @@ -59,7 +71,9 @@ export class TraefikConfigManager { // Initialize local certificate state this.lastLocalCertificateState = await this.scanLocalCertificateState(); - logger.info(`Found ${this.lastLocalCertificateState.size} existing certificate directories`); + logger.info( + `Found ${this.lastLocalCertificateState.size} existing certificate directories` + ); // Run initial check await this.HandleTraefikConfig(); @@ -94,40 +108,47 @@ export class TraefikConfigManager { /** * Scan local certificate directories to build current state */ - private async scanLocalCertificateState(): Promise> { + private async scanLocalCertificateState(): Promise< + Map< + string, + { + exists: boolean; + lastModified: Date | null; + expiresAt: Date | null; + } + > + > { const state = new Map(); const certsPath = config.getRawConfig().traefik.certificates_path; - + try { if (!fs.existsSync(certsPath)) { return state; } const certDirs = fs.readdirSync(certsPath, { withFileTypes: true }); - + for (const dirent of certDirs) { if (!dirent.isDirectory()) continue; - + const domain = dirent.name; const domainDir = path.join(certsPath, domain); const certPath = path.join(domainDir, "cert.pem"); const keyPath = path.join(domainDir, "key.pem"); const lastUpdatePath = path.join(domainDir, ".last_update"); - + const certExists = await this.fileExists(certPath); const keyExists = await this.fileExists(keyPath); const lastUpdateExists = await this.fileExists(lastUpdatePath); - + let lastModified: Date | null = null; let expiresAt: Date | null = null; - + if (lastUpdateExists) { try { - const lastUpdateStr = fs.readFileSync(lastUpdatePath, "utf8").trim(); + const lastUpdateStr = fs + .readFileSync(lastUpdatePath, "utf8") + .trim(); lastModified = new Date(lastUpdateStr); } catch { // If we can't read the last update, fall back to file stats @@ -139,7 +160,7 @@ export class TraefikConfigManager { } } } - + state.set(domain, { exists: certExists && keyExists, lastModified, @@ -149,7 +170,7 @@ export class TraefikConfigManager { } catch (error) { logger.error("Error scanning local certificate state:", error); } - + return state; } @@ -161,40 +182,51 @@ export class TraefikConfigManager { if (!this.lastCertificateFetch) { return true; } - + // Fetch if it's been more than 24 hours (for renewals) const dayInMs = 24 * 60 * 60 * 1000; - const timeSinceLastFetch = Date.now() - this.lastCertificateFetch.getTime(); + const timeSinceLastFetch = + Date.now() - this.lastCertificateFetch.getTime(); if (timeSinceLastFetch > dayInMs) { logger.info("Fetching certificates due to 24-hour renewal check"); return true; } - + // Fetch if domains have changed - if (this.lastKnownDomains.size !== currentDomains.size || - !Array.from(this.lastKnownDomains).every(domain => currentDomains.has(domain))) { + if ( + this.lastKnownDomains.size !== currentDomains.size || + !Array.from(this.lastKnownDomains).every((domain) => + currentDomains.has(domain) + ) + ) { logger.info("Fetching certificates due to domain changes"); return true; } - + // Check if any local certificates are missing or appear to be outdated for (const domain of currentDomains) { const localState = this.lastLocalCertificateState.get(domain); if (!localState || !localState.exists) { - logger.info(`Fetching certificates due to missing local cert for ${domain}`); + logger.info( + `Fetching certificates due to missing local cert for ${domain}` + ); return true; } - + // Check if certificate is expiring soon (within 30 days) if (localState.expiresAt) { - const daysUntilExpiry = (localState.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24); + const daysUntilExpiry = + (localState.expiresAt.getTime() - Date.now()) / + (1000 * 60 * 60 * 24); if (daysUntilExpiry < 30) { - logger.info(`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)`); + logger.info( + `Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)` + ); return true; } } } - + return false; } @@ -234,7 +266,8 @@ export class TraefikConfigManager { } // Scan current local certificate state - this.lastLocalCertificateState = await this.scanLocalCertificateState(); + this.lastLocalCertificateState = + await this.scanLocalCertificateState(); // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) let validCertificates: Array<{ @@ -248,19 +281,33 @@ export class TraefikConfigManager { if (this.shouldFetchCertificates(domains)) { // Get valid certificates for active domains - validCertificates = await this.getValidCertificatesForDomains(domains); + if (config.isHybridMode()) { + validCertificates = + await getValidCertificatesForDomainsHybrid(domains); + } else { + validCertificates = + await getValidCertificatesForDomains(domains); + } this.lastCertificateFetch = new Date(); this.lastKnownDomains = new Set(domains); - - logger.info(`Fetched ${validCertificates.length} certificates from remote`); + + logger.info( + `Fetched ${validCertificates.length} certificates from remote` + ); // Download and decrypt new certificates await this.processValidCertificates(validCertificates); } else { - const timeSinceLastFetch = this.lastCertificateFetch ? - Math.round((Date.now() - this.lastCertificateFetch.getTime()) / (1000 * 60)) : 0; - logger.debug(`Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`); - + const timeSinceLastFetch = this.lastCertificateFetch + ? Math.round( + (Date.now() - this.lastCertificateFetch.getTime()) / + (1000 * 60) + ) + : 0; + logger.debug( + `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` + ); + // Still need to ensure config is up to date with existing certificates await this.updateDynamicConfigFromLocalCerts(domains); } @@ -276,7 +323,18 @@ export class TraefikConfigManager { // Send domains to SNI proxy try { - const [exitNode] = await db.select().from(exitNodes).limit(1); + let exitNode; + if (config.getRawConfig().gerbil.exit_node_name) { + const exitNodeName = + config.getRawConfig().gerbil.exit_node_name!; + [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.name, exitNodeName)) + .limit(1); + } else { + [exitNode] = await db.select().from(exitNodes).limit(1); + } if (exitNode) { try { await axios.post( @@ -300,7 +358,9 @@ export class TraefikConfigManager { } } } else { - logger.error("No exit node found. Has gerbil registered yet?"); + logger.error( + "No exit node found. Has gerbil registered yet?" + ); } } catch (err) { logger.error("Failed to post domains to SNI proxy:", err); @@ -320,21 +380,31 @@ export class TraefikConfigManager { domains: Set; traefikConfig: any; } | null> { + let traefikConfig; try { - const resp = await axios.get( - `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/traefik-config`, - await tokenManager.getAuthHeader() - ); - - if (resp.status !== 200) { - logger.error( - `Failed to fetch traefik config: ${resp.status} ${resp.statusText}`, - { responseData: resp.data } + if (config.isHybridMode()) { + const resp = await axios.get( + `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/traefik-config`, + await tokenManager.getAuthHeader() + ); + + if (resp.status !== 200) { + logger.error( + `Failed to fetch traefik config: ${resp.status} ${resp.statusText}`, + { responseData: resp.data } + ); + return null; + } + + traefikConfig = resp.data.data; + } else { + const currentExitNode = await getCurrentExitNodeId(); + traefikConfig = await getTraefikConfig( + currentExitNode, + config.getRawConfig().traefik.site_types ); - return null; } - const traefikConfig = resp.data.data; const domains = new Set(); if (traefikConfig?.http?.routers) { @@ -445,16 +515,20 @@ export class TraefikConfigManager { /** * Update dynamic config from existing local certificates without fetching from remote */ - private async updateDynamicConfigFromLocalCerts(domains: Set): Promise { - const dynamicConfigPath = config.getRawConfig().traefik.dynamic_cert_config_path; - + private async updateDynamicConfigFromLocalCerts( + domains: Set + ): Promise { + const dynamicConfigPath = + config.getRawConfig().traefik.dynamic_cert_config_path; + // Load existing dynamic config if it exists, otherwise initialize let dynamicConfig: any = { tls: { certificates: [] } }; if (fs.existsSync(dynamicConfigPath)) { try { const fileContent = fs.readFileSync(dynamicConfigPath, "utf8"); dynamicConfig = yaml.load(fileContent) || dynamicConfig; - if (!dynamicConfig.tls) dynamicConfig.tls = { certificates: [] }; + if (!dynamicConfig.tls) + dynamicConfig.tls = { certificates: [] }; if (!Array.isArray(dynamicConfig.tls.certificates)) { dynamicConfig.tls.certificates = []; } @@ -495,67 +569,6 @@ export class TraefikConfigManager { } } - /** - * Get valid certificates for the specified domains - */ - private async getValidCertificatesForDomains(domains: Set): Promise< - Array<{ - id: number; - domain: string; - certFile: string | null; - keyFile: string | null; - expiresAt: Date | null; - updatedAt?: Date | null; - }> - > { - if (domains.size === 0) { - return []; - } - - const domainArray = Array.from(domains); - - try { - const response = await axios.get( - `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/certificates/domains`, - { - params: { - domains: domainArray - }, - headers: (await tokenManager.getAuthHeader()).headers - } - ); - - if (response.status !== 200) { - logger.error( - `Failed to fetch certificates for domains: ${response.status} ${response.statusText}`, - { responseData: response.data, domains: domainArray } - ); - return []; - } - - // logger.debug( - // `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains` - // ); - - return response.data.data; - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error getting certificates:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error getting certificates:", error); - } - return []; - } - } - /** * Process valid certificates - download and decrypt them */ @@ -640,7 +653,7 @@ export class TraefikConfigManager { logger.info( `Certificate updated for domain: ${cert.domain}` ); - + // Update local state tracking this.lastLocalCertificateState.set(cert.domain, { exists: true, diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 4ec2908c..918df3fd 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -11,6 +11,35 @@ let currentExitNodeId: number; const redirectHttpsMiddlewareName = "redirect-to-https"; const badgerMiddlewareName = "badger"; +export async function getCurrentExitNodeId(): Promise { + if (!currentExitNodeId) { + if (config.getRawConfig().gerbil.exit_node_name) { + const exitNodeName = config.getRawConfig().gerbil.exit_node_name!; + const [exitNode] = await db + .select({ + exitNodeId: exitNodes.exitNodeId + }) + .from(exitNodes) + .where(eq(exitNodes.name, exitNodeName)); + if (exitNode) { + currentExitNodeId = exitNode.exitNodeId; + } + } else { + const [exitNode] = await db + .select({ + exitNodeId: exitNodes.exitNodeId + }) + .from(exitNodes) + .limit(1); + + if (exitNode) { + currentExitNodeId = exitNode.exitNodeId; + } + } + } + return currentExitNodeId; +} + export async function traefikConfigProvider( _: Request, res: Response @@ -18,34 +47,12 @@ export async function traefikConfigProvider( try { // First query to get resources with site and org info // Get the current exit node name from config - if (!currentExitNodeId) { - if (config.getRawConfig().gerbil.exit_node_name) { - const exitNodeName = - config.getRawConfig().gerbil.exit_node_name!; - const [exitNode] = await db - .select({ - exitNodeId: exitNodes.exitNodeId - }) - .from(exitNodes) - .where(eq(exitNodes.name, exitNodeName)); - if (exitNode) { - currentExitNodeId = exitNode.exitNodeId; - } - } else { - const [exitNode] = await db - .select({ - exitNodeId: exitNodes.exitNodeId - }) - .from(exitNodes) - .limit(1); + await getCurrentExitNodeId(); - if (exitNode) { - currentExitNodeId = exitNode.exitNodeId; - } - } - } - - let traefikConfig = await getTraefikConfig(currentExitNodeId, config.getRawConfig().traefik.site_types); + let traefikConfig = await getTraefikConfig( + currentExitNodeId, + config.getRawConfig().traefik.site_types + ); traefikConfig.http.middlewares[badgerMiddlewareName] = { plugin: { @@ -80,7 +87,10 @@ export async function traefikConfigProvider( } } -export async function getTraefikConfig(exitNodeId: number, siteTypes: string[]): Promise { +export async function getTraefikConfig( + exitNodeId: number, + siteTypes: string[] +): Promise { // Define extended target type with site information type TargetWithSite = Target & { site: { @@ -135,7 +145,7 @@ export async function getTraefikConfig(exitNodeId: number, siteTypes: string[]): eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId) ), - inArray(sites.type, siteTypes), + inArray(sites.type, siteTypes) ) ); @@ -438,7 +448,10 @@ export async function getTraefikConfig(exitNodeId: number, siteTypes: string[]): return false; } } else if (target.site.type === "newt") { - if (!target.internalPort || !target.site.subnet) { + if ( + !target.internalPort || + !target.site.subnet + ) { return false; } } From 7dc74cb61b3e5eb5fddd97aec0bad030b7194c4e Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 21:45:17 -0700 Subject: [PATCH 153/219] Fix import for traefikConfig --- server/hybridServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index c0d342cf..e38ca088 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -3,7 +3,7 @@ import config from "@server/lib/config"; import { createWebSocketClient } from "./routers/ws/client"; import { addPeer, deletePeer } from "./routers/gerbil/peers"; import { db, exitNodes } from "./db"; -import { TraefikConfigManager } from "./lib/remoteTraefikConfig"; +import { TraefikConfigManager } from "./lib/traefikConfig"; import { tokenManager } from "./lib/tokenManager"; import { APP_VERSION } from "./lib/consts"; import axios from "axios"; From 97fcaed9b46b4f965ed5a884354aea99a864b39e Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 21:58:27 -0700 Subject: [PATCH 154/219] Optionally use file mode --- server/index.ts | 5 +++++ server/lib/readConfigFile.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server/index.ts b/server/index.ts index 58a0fd24..c8aaff73 100644 --- a/server/index.ts +++ b/server/index.ts @@ -11,6 +11,7 @@ import { createHybridClientServer } from "./hybridServer"; import config from "@server/lib/config"; import { setHostMeta } from "@server/lib/hostMeta"; import { initTelemetryClient } from "./lib/telemetry.js"; +import { TraefikConfigManager } from "./lib/traefikConfig.js"; async function startServers() { await setHostMeta(); @@ -30,6 +31,10 @@ async function startServers() { hybridClientServer = await createHybridClientServer(); } else { nextServer = await createNextServer(); + if (config.getRawConfig().traefik.file_mode) { + const monitor = new TraefikConfigManager(); + await monitor.start(); + } } let integrationServer; diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 8107385c..b13d477c 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -156,7 +156,8 @@ export const configSchema = z .optional() .default("./dynamic/router_config.yml"), static_domains: z.array(z.string()).optional().default([]), - site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]) + site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]), + file_mode: z.boolean().optional().default(false) }) .optional() .default({}), From 9d561ba94dee450d70b2204c1570f88539e51519 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 22:01:30 -0700 Subject: [PATCH 155/219] Remove bad import --- server/routers/external.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/routers/external.ts b/server/routers/external.ts index 95add3ed..baa1fd69 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -15,7 +15,6 @@ import * as accessToken from "./accessToken"; import * as idp from "./idp"; import * as license from "./license"; import * as apiKeys from "./apiKeys"; -import * as hybrid from "./hybrid"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, From 117062f1d1f487ffd3f05e6e687e327773d9ac74 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 17 Aug 2025 22:18:25 -0700 Subject: [PATCH 156/219] One start command --- Dockerfile | 2 +- package.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index f84cb36d..996ef057 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,4 +43,4 @@ COPY server/db/names.json ./dist/names.json COPY public ./public -CMD ["npm", "run", "start:$DATABASE"] +CMD ["npm", "run", "start"] diff --git a/package.json b/package.json index 7b3464a8..e5f2238b 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ "db:clear-migrations": "rm -rf server/migrations", "build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", - "start:sqlite": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", - "start:pg": "DB_TYPE=pg NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", + "start": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", "email": "email dev --dir server/emails/templates --port 3005", "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs" }, From d2073184947664b6d25c49277210d881e210adb2 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 18 Aug 2025 12:06:01 -0700 Subject: [PATCH 157/219] remove org from get client route --- server/routers/client/getClient.ts | 15 +++++++-------- server/routers/external.ts | 4 ++-- server/routers/integration.ts | 4 ++-- .../settings/clients/[clientId]/layout.tsx | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 8f01e87d..d362526f 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -13,17 +13,16 @@ import { OpenAPITags, registry } from "@server/openApi"; const getClientSchema = z .object({ - clientId: z.string().transform(stoi).pipe(z.number().int().positive()), - orgId: z.string() + clientId: z.string().transform(stoi).pipe(z.number().int().positive()) }) .strict(); -async function query(clientId: number, orgId: string) { +async function query(clientId: number) { // Get the client const [client] = await db .select() .from(clients) - .where(and(eq(clients.clientId, clientId), eq(clients.orgId, orgId))) + .where(and(eq(clients.clientId, clientId))) .limit(1); if (!client) { @@ -47,9 +46,9 @@ export type GetClientResponse = NonNullable>>; registry.registerPath({ method: "get", - path: "/org/{orgId}/client/{clientId}", + path: "/client/{clientId}", description: "Get a client by its client ID.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.Client], request: { params: getClientSchema }, @@ -75,9 +74,9 @@ export async function getClient( ); } - const { clientId, orgId } = parsedParams.data; + const { clientId } = parsedParams.data; - const client = await query(clientId, orgId); + const client = await query(clientId); if (!client) { return next( diff --git a/server/routers/external.ts b/server/routers/external.ts index 65dc6108..9d1de051 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -134,9 +134,9 @@ authenticated.get( ); authenticated.get( - "/org/:orgId/client/:clientId", + "/client/:clientId", verifyClientsEnabled, - verifyOrgAccess, + verifyClientAccess, verifyUserHasAction(ActionsEnum.getClient), client.getClient ); diff --git a/server/routers/integration.ts b/server/routers/integration.ts index ee707333..d2734fd3 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -526,9 +526,9 @@ authenticated.get( ); authenticated.get( - "/org/:orgId/client/:clientId", + "/client/:clientId", verifyClientsEnabled, - verifyApiKeyOrgAccess, + verifyApiKeyClientAccess, verifyApiKeyHasAction(ActionsEnum.getClient), client.getClient ); diff --git a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx index 804162a2..d137b00c 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx @@ -21,7 +21,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { let client = null; try { const res = await internal.get>( - `/org/${params.orgId}/client/${params.clientId}`, + `/client/${params.clientId}`, await authCookieHeader() ); client = res.data.data; From cd348201381d8ad2bb3c13a1ce0f4825a1a4c75c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 18 Aug 2025 12:06:59 -0700 Subject: [PATCH 158/219] prompt for convert node in installer --- install/main.go | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/install/main.go b/install/main.go index b08f0073..ca68a769 100644 --- a/install/main.go +++ b/install/main.go @@ -215,7 +215,7 @@ func main() { } } else { fmt.Println("Looks like you already installed, so I am going to do the setup...") - + // Read existing config to get DashboardDomain traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") if err != nil { @@ -226,19 +226,28 @@ func main() { config.DashboardDomain = traefikConfig.DashboardDomain config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail config.BadgerVersion = traefikConfig.BadgerVersion - + // Show detected values and allow user to confirm or re-enter fmt.Println("Detected existing configuration:") fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain) fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail) fmt.Printf("Badger Version: %s\n", config.BadgerVersion) - + if !readBool(reader, "Are these values correct?", true) { config = collectUserInput(reader) } } } + // Check if Pangolin is already installed with hybrid section + if checkIsPangolinInstalledWithHybrid() { + fmt.Println("\n=== Convert to Self-Host Node ===") + if readBool(reader, "Do you want to convert this Pangolin instance into a manage self-host node?", true) { + fmt.Println("hello world") + return + } + } + if !checkIsCrowdsecInstalledInCompose() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed @@ -276,7 +285,7 @@ func main() { // Setup Token Section fmt.Println("\n=== Setup Token ===") - + // Check if containers were started during this installation containersStarted := false if (isDockerInstalled() && chosenContainer == Docker) || @@ -285,7 +294,7 @@ func main() { containersStarted = true printSetupToken(chosenContainer, config.DashboardDomain) } - + // If containers weren't started or token wasn't found, show instructions if !containersStarted { showSetupTokenInstructions(chosenContainer, config.DashboardDomain) @@ -354,7 +363,7 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") - + // Set default dashboard domain after base domain is collected defaultDashboardDomain := "" if config.BaseDomain != "" { @@ -816,7 +825,7 @@ func waitForContainer(containerName string, containerType SupportedContainer) er func printSetupToken(containerType SupportedContainer, dashboardDomain string) { fmt.Println("Waiting for Pangolin to generate setup token...") - + // Wait for Pangolin to be healthy if err := waitForContainer("pangolin", containerType); err != nil { fmt.Println("Warning: Pangolin container did not become healthy in time.") @@ -938,3 +947,24 @@ func checkPortsAvailable(port int) error { } return nil } + +func checkIsPangolinInstalledWithHybrid() bool { + // Check if docker-compose.yml exists (indicating Pangolin is installed) + if _, err := os.Stat("docker-compose.yml"); err != nil { + return false + } + + // Check if config/config.yml exists and contains hybrid section + if _, err := os.Stat("config/config.yml"); err != nil { + return false + } + + // Read config file to check for hybrid section + content, err := os.ReadFile("config/config.yml") + if err != nil { + return false + } + + // Check for hybrid section + return bytes.Contains(content, []byte("hybrid:")) +} From c29cd05db8019e2e0fc662c728942a67adb5df22 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 18 Aug 2025 11:55:57 -0700 Subject: [PATCH 159/219] Update to pull defaults from var --- server/lib/readConfigFile.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index b13d477c..fa05aebd 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -145,16 +145,16 @@ export const configSchema = z additional_middlewares: z.array(z.string()).optional(), cert_resolver: z.string().optional().default("letsencrypt"), prefer_wildcard_cert: z.boolean().optional().default(false), - certificates_path: z.string().default("./certificates"), + certificates_path: z.string().default("/var/certificates"), monitor_interval: z.number().default(5000), dynamic_cert_config_path: z .string() .optional() - .default("./dynamic/cert_config.yml"), + .default("/var/dynamic/cert_config.yml"), dynamic_router_config_path: z .string() .optional() - .default("./dynamic/router_config.yml"), + .default("/var/dynamic/router_config.yml"), static_domains: z.array(z.string()).optional().default([]), site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]), file_mode: z.boolean().optional().default(false) From 9bdf31ee972c40239a90e5d5504d15c8da616487 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 18 Aug 2025 12:22:32 -0700 Subject: [PATCH 160/219] Add csrf to auth --- server/lib/tokenManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts index e6a03067..45f280ba 100644 --- a/server/lib/tokenManager.ts +++ b/server/lib/tokenManager.ts @@ -156,7 +156,8 @@ export class TokenManager { async getAuthHeader() { return { headers: { - Authorization: `Bearer ${await this.getToken()}` + Authorization: `Bearer ${await this.getToken()}`, + "X-CSRF-Token": "x-csrf-protection", } }; } From ac8b54639356ff1490eea306919d252159a90ece Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 18 Aug 2025 14:29:06 -0700 Subject: [PATCH 161/219] add sqlite 1.9.0 migration --- config/db/db.sqlite.bak | Bin 323584 -> 0 bytes server/setup/scriptsSqlite/1.9.0.ts | 169 ++++++++++++++++++++++++++-- 2 files changed, 159 insertions(+), 10 deletions(-) delete mode 100644 config/db/db.sqlite.bak diff --git a/config/db/db.sqlite.bak b/config/db/db.sqlite.bak deleted file mode 100644 index 9d0b3db301aba78725e447cb3c84bec1ae023b7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 323584 zcmeI532+-(dfx#OBnW^2sDpwu94%YJnISRMLGX~onbFKZBq(vkYk178c2h+((T`-W zzy_wf!DEulni6N%E|;&^+1lEqTz0Z9$5))%#I^BJiJinwoV6?7*yT7*%C5CzdtJ5@ zyW%TryDIPN*WKtQ0129rW+eHK;?eJW*Y|$k_r0T=0GD2$QB5YTm-H>!ltuzwfnYH3 znj{4RferpW&%d45D8C3f3I03ixo&j15jg+O$J=;rTk`j)%yjavl7F83ljI*Je=qi> z$Z9y${>8Sr_B*X#Z~1b|sg}=aRfM5RK5qw33%p&14%)mlhVMXS0hpr5m}MQg&r|etM4Ao6XHFOLO!5 zdu3+kEU%==TP!b`Y}fRO&AoCxvBK8nO3_>_71^v@F00yxs7o!`dGq>4O_p3SOAC5w zTQ$^@R@HDaH}dMJk@nQY>0m(B6t-);Rpf5EN@K9nt4<%Q8RzQg?X$^n zs<$_I&~K^Cc2#q(q%gz1=<+M=%7xZW@`0I8Rur8XMjgX4O)2wn@L|{3yj)o;s)ZYD zuTEZ3HB6>0l=M2h$oQOQVUt{xfKtmO+>&=EN}9$BCO11*#`er;`u6LIaB6%!cyLAF z+LG1c8%$T%)q+fACoFb#rO87e>lSk2!R@liV5k^OcRj~(2O0^ZZt7qua7F8g*=||o z{OdUvZ*6LRF*ki}&I-(dyxnJ7TFg!57ISkGxh2UG<_CC0=hM>soV2nqNdd2wB-vs~ zvdP>`j>`(NQIHkJnGSb`Q$0PwgO@C(N{Oqi8TPNZpPDTdm_))IcB6nIXOht(soQt=ovOSFg z-Nv+*_Ev>y1$HRe=_7t9bAV>uv%F^d;A2tCYTp#L+AI`R zrkRHQE9Pq>Z%f~+bC>iD*T_EKwkIn$SjZv`mIXdVxbp)QGb(Eut2dXd?U~G&uFmyT z*pkgxsxt@)i?U%}l{IBYRm{zmvcf0j>I6cW3%*6B>Nl7V}`4X*PAZK0wSHr*z8jz3NXK7W{UnZd)<$!aQ6YZ`VYe;(@IDZmj^2hKIO-@WDo zgPD?6DHi#Juo?-c`uc+Jws~A@i7#cgds&y!dM;+=2saZxM}Fg=^}3G1TWgf38jGwb z?==;we$sBAejtwf-_NvI1KZZv-!cgavzytJA*1PiBWwll9kGnbWt9hqVgKszleMLS zsz|ue*l5aa5QL)kb=lzFTC<2#$V_a?dcC!uf@?zTd+OBW1}l}S!RgMI{uqj;&vo2w z&a}t5nz1h;w+GtA)~MHVLm{5mH{6SGJy5(rT~uE06Jb|_!0$|=8!K?U(PeALjqWMX zl-X-s%87n&^IUe{69bRf*V@9V{{G;>s~%KYv3n9~+KKjh$n3m(ViQ{k*ZDj|KWK8U zCjM{~V&dm1(3EYW>33SKh}>Ti1Z{TWSLhHmA+>o~2^Y$}Ba*uTKk*m^T`kj&Rm2ky zj?i6$Lv+c5LtM1&r-R|t*jRACV(GiOs_5$5Zx`9>mb#%^TVbP~ti?ByXX+JshK_{% zr7F@=ZgDz0JwwZ28W`0I;>_$ z$`8+1UgkeWM);2x&hj58`uLCTBXRySnEaOk{)HC^fB*=900@8p2!H?xfB*=900@A< zqeI|Eur073ZyU;7VwthA!ljF&%II2#T^t)5UAsJTd2DEXWW1orqw8arE?q1rmq$j{ zmCIw9v60J#wc)kVQH5Q~42=y3Kljx4{`4u0%GyZ1$ zTKrP{h4_)q|JV7Ooj=?8!=0b){D#iW&V|nL&NH3K*x$tdAolCAFUP(V`~KMHV&4>d zJ64S4V{@^WV}r3%u}JhUqQ4XUFVUZheqZ#vqVGn_(NAy%@d5!5009sH0T2KI5O{P5 z9A5~X3k3L^txQ=r`CpBTw`RfTgT6o1|73gfe(UjBYlW@fFw-cQbi z_#dazyWK?E=)MuU;1y)WU+y0HXsACB;BU4N#dveZ@oCb;71;&5u7{p;G!bRHu8~Bj zAtZ^Z&>1e-WQAM2p=qTnM|;)UbeKe-ZwDEcFdzMh0>xa7orh=(WW?I zy@4q??x-u$M%U%)h=i!?Qgr~foF-&qX9YWb7r zLt_?Xr{y;{`MXJ|(Ua#WhxQ%`ymHwvc1pSudm+?gb+W?W zpqNOLJ%qKa^Z=!~2Xk{!I@wR~`8z-=dLX zjN^TWq*mGPXAU=E6+Uu?f`b?=aTb;+E|G(f$f}s=C7pG)RoZ4GIQ|i8UZkhBZPm2? z_Wjst3RtRCl^Kbrsj5natg4BgDh}!@{*=>`)p6{UjgC@@lT_CB5K5;gZF>e!cb%xZ z>9VYIrpa!{lSSHyAGbYSq`Qt)-Po2Kt-7(096dq-BMjz1J<{bMawJAFNnPWTjxUr8)=elhwd(XVzMkCkITl6W=tf0Mrz|D8@P{(Z@K z{M!<5#s4a)Bz_|Kl|(%8LZT=7S$^sd$FIbnkN;%+m*VlxXJh{ z-k^IgVK&*?7o;7reJf$6yY+%6ZQmfcaiq0B=-xG;t@1}n1y^Dho9=3T&Q?J*cbx>H ze<8S*Xgw3G-b9$9eD_Sx+s!%Z@H0JiGg)m>NG7T+3c=NA>nR!zPh~q>Pg}b4YvVN< zroh3IuTskG=oLx{wRqkYODFf%!OKTm(^f0Ie?iQ6$wnql{4Y8>ili}K9fXivt|~1A zm#Rt&!Nn7;8CxCaAVv2K#vJ%wp)pE@{UiOTukWe&HyJ$DKET|oRG?C57a?@GP*2%fV&)G212w4XgBq)v>!!}YDA&ydd( zNIG&_k~8E6&gs#RNY(1yfnI9)@XdjbQ2bF1&%J@uRIhq#;OQy=>ZHf%!|L%Vn-nEZ zQ9;`cc=9AAZS(VFs_KN|K!3vVQjs*eZ66lN<5d?Gf@4(|6@sG_2Es57!6Ob4M__bO zCmvTxQYF8uCP?IW(|C2r#J1BJG9`?dJ!DEmt3#$_q&j3uc2GUh8F!a3rG&mb)o#xU z{IZQQgaSMjqH#Jl4dAOHd&00JNY0w4eaAOHd& z00Iw#0G9kxx1d( zD9L5bP-IIjPuYQ%j=^4`%C@SGt(ofXEp2#b`_f)%OV-X8N?VT7F9bV!Mj|v;g=eYS zn7OGMsv&D^M+iKv(&zuRC%+?*{J!K@lRuvPv*a)G1YRHj0w4eaAOHd&00JNY0w4ea zAOHdnm%y>s!Qi1=6zwECbpN4^WQT4sgh+PiMnbDq*MGyHCED5(v2P9F{r?ZwoM;^c zKmY_l00ck)1V8`;KmY_l00bTd0yzIa3RZv)AOHd&00JNY0w4eaAOHd&00JQJa0!Ud z|NBZH`IU#;4_XHS5C8!X009sH0T2KI5C8!X009tqfCSD3TUXlH=-AM@a&hh4m@3G_-zsXnZJh>2fCcxu?GOxBeBq|38@gwE+LZ3j{y_1V8`;KmY_l z00ck)1V8`;K;SVW(BHZeY342f{{H_lGc42v0T2KI5C8!X009sH0T2KI5CDPqKmh0e z_W%u5KmY_l00ck)1V8`;KmY_l00cnbF(VL9{$=1u%c(%JHTk27<;1VVkHmgA`ZKXF zMOMS1_Aj>m{kFN*ueW@;<)ba9f_=Q`-^SOzaWtGdb}V>j)|A(ZOjXMK>)6C%F1wtQ zma|u9a#Eg4^3p&wLKo9YUQ#uaZ7^M0Se%~CF5Z-Gl-y$a>Xny=%sD` zuOLfWRl~{LRCZ-%Sz4Ei26MZQrY{_6PfeT-22@R9yT)5Z)nu#Ggq2?1EE(o3Gv(Ec zb9MAnUE$Q})4~1MElQ5q`F+IC%MlA`PKwA`6uj8Hpb9#ZO{Qecs{T%PG=2MQGMwt| z4IcDc8n9i}oGU5Ja4)+2I=FJ7gp+(==93jgXNFP7uuN0R93&q_jm^uIwW3v4Y9X&Xut}ljC|l5l)Sd2M?|YT>L+U z7T;jHx~>*vDm!7Zt1C?&0$I0^6Ax~eO$I~7V7luFj&*4yjJm0VrN9-fBWAm0mGiIX zE4;O-`NiDywK*#=2l95GX=yPxm0QfsP2`p&OPC+v5uHy<^K;V5!XyQ}R+40kCCMgp zGdV6R$VNd{STubu-k!?#GzuE=FISA!jD3-~J=_^i_4EV}Ub0MJi|t==KV(}fsEUM3 zsZqGB$p)LW{sn9uK82FDuJUO@lQ)=W#;QLb5dZ3QM5%4dRhp~0&JEXGNmEx@)k!&D zF*yx+CZ>g0dunhnc+)~=PfP}TtHQJbJCyA7Av}~hK(p>yUNe30v8ZK+Zwgy&7K$p< zOvC;a^RQ4^u8Pc$hj_O+{)=!_MT-L)|+CIAZF+x#sq}*L+|w zQ_?EMBA;YeBjHqEU+~>Fk83UQwbXVm>oQu;#jG6RX2R#lZydB<*D-i&jq+4ukrm~= zree=e+U?U1#Bu-onHFnc+Zy{@CLv*VGn+DGG`(+xt>C>QmNB`k^58J+Umbq3wp36R z2{#%W&7lo~P}III8{AuK7I6xhiA`CrxAs$TO^9twotoTWrBXFG-3ibiLwup>xZ6Tw zk8?F+Uqo&Xw2Q4#ujPh9Jg;xK7vXxKc!9d8yxu3mt_FeMnM5~M;CQ3U){Yz9Q=loc z*SM4u{odxe?7k-k9O^c;C{u@cXd_K)wka+vehkhL$|iVMm<@JZzRvuEAk8-3HeJ^ zq@~>AbauwO1?I~0*CwAqYNChB?UT-`Oe@J2-)j}9c=}pPd+HNo!3L+YdOECT>PcJq zLB4|y^Yr}xXvk?)dqR?+O1vSZg=hKic+m=nJ90+d3Hd zg>Yve(D`|)*Yszd%jjbLa9xetSI}eo^rfrzjjh1g8S9@@xI5SnV=6o3Eq5~Nh@SWRMV%5=w|8vmo%kBxG-`AlZg_san_JY{-0Kql zHR(YemiwP+PrcOLC|qgvUd_Z?)0&Zt$zv&QEY>rn1$>B;h=7DMr`%+^5P?|H{nz~8nI z)2i<&(B!J$-+Nbc@(G@slmC3?16&bke{k3RwtLKRCeoUnrCY*J>b)Fchi^YOZ56((wA4AYwmiRm7A%aZ}KB>^S9JQ8-8fk zDt|vm$7s60=Wbr5u^n?YLq9)qJN%T`XS`(vE6M2lw4VlvnuO4JpK(vG;`heHO?Kxd z?4dJyjgz^DUhnzky`G52PPM0|`x-eJ5pZ41lX!Z+eA2pAVLO;7%X8UNi_>nbNvhP= z`}Qrn5x;YhkWXCdy>HLBw`*@psc`Dlso+76s8IEUY$J5*J;RM?4(_1@$QPeG*rznWq$A1E$Gd(_B@B~v+#xemE*$hZ(4Te z&+O*)4e>MO*GmW;-}XJ)^LEq7wtBs1biDB4hjac_UpzzfdpDO^&P^)Z>-MAV-w)9% z?p!-&Ek>VqABoPZN`XIT;!hL^nVUFXi_^_>sm}A~ikqVH{fCu+<;J4dDq`uY>lepPpf>zR#`T-HvsIPGk!>e!m8?%vXdceXDjg00;>)UQ(7l2vVt&i}#WZwL4n zULXJhAOHd&00JNY0w4eaAOHd&00NIMfu3Nf+rHPp&;6O9q4VQI=QAUh&JSG}9-_bh z4<`RIz`yVU0T2KI5C8!X009sH0T2KI5C8!Xc)|#TTe~ClX8`#8zbDMF7!C-400@8p z2!H?xfB*=900@8p2-G2f^M4&E5P<*)fB*=900@8p2!H?xfB*=9z!OFQpa1`a85F|- z0T2KI5C8!X009sH0T2KI5CDNX1n~U74itz$00ck)1V8`;KmY_l00ck)1VG>kBS7c> zNXwT3q4iL2;z#3uuk&2=AGW{K`Wr1@YW)|^P+LpR6@J?U+RwLJSD#rv7f$u|2ETR3 zl-G((XGW=_7ntE*WF{7K+2x$HoV_}elk%=SFAYQ^ZgN`5OR8qF4W>&Ai_^2&#hcQN z+)XLFvOGUM$D5eV%`HoF^Za{dX67ufX{aXi*7D|+^bN`~*{FRKHH&sYQc__?L08LU%WBjWbS9fj z@znIjmfwv;-1|L<%Z&U$-pOjpZ@4o4hCOurtveaig>%gjrpXtZ@y~BO{z5o)?p*NO zKH>8V$0402o%H$o9^qtHW7rBu??mb~hSO>zTh|)Pi#*?ixs}pf$;|DVO!LeqjVDOe zSY&Ti_=oXWKJ0UmiotYyc#dzAtCVHK*eU6XX9$*9)e0rWyDpG)$C(rlm97&vAC%>5 z`tgs`?_@S`?qX`wu2s{4hnT;0#~T)Gq`Vtn*`_?Xwma7n4SEy#)S0K`d+W<<-}=2( zSGo7a(C;wyyYkM?1Np->ni^mbF+EgJXW2_DtmRw9|tPfI1VGb)xXhT@M|&KNA1W_^0B<_)7f6xD;>i{DaP4>io&h z@9+H1&fA@=bGmb|Gamcn*ssQZD)v3GgP0ndja`bJiN&IS9{ug;m!n^bej)lD(RZT7 z=--J>MTeq2(YDB+ME*zQXCprp`L4)bM2Wl}xg6<@v~~Px$8T^o@d5!5009sH0T2KI zo4|>sP_VzdrKlR_A~Q-Ay}%5Ue;r>81^c>NHkd0WY4Y_@@RTK;*EdLTav>Bs*UkTf zv?=Q*mra(oWL2Y_)O;v1*sZW4GnqeQjxq{5lMiJanGHpHyLpemUaStdDt&4!6v^1S zJASxWA>T2~Q=^W(y*XCLPmNGDLBX3z)t(xrJTdmec_%aE5SCvRxWgOErCUsR(8(dn z5iMrjENZXkLMSrAZK5%q8#`OAN10Eb4@JhvU`|hOZmK4u4y@dBl*|3EZthcONl{Pk zf~pltiZ%A0LF&|-Xa6sGYBu>oDAGfnt{6-w!O=9?R(OCb8z31k#7*nSj`fofWkn&u zV#&t*yo;Gj98E3Er=D}O>^XueT3IKbwL{CPK+TDqzC$@~om0;oZqBXLeTD*wyl~n%?y;2I!%)fHBgmXS{{9xno+5+ z+sx6PDn^ISk*6FDT&0qyZ2V4{qbI4J?Tt3oqbZWvUTDjXpQwh*vaEC4kc!8;oyZb0 zOT8n!J3b(sCm-i%rJ+C9toIg;PB}!8^UCyvFAhK2_6FFMho^v6#y) z=cMKA)tQ`>x21V$AR5W5N?tPAt|=`nPS0i+Z%Q|EH>J7xWod3@X67s}P~lU?v{T5N z!FJ24&Wx;?muMri!E|rEsrkj+^tCywxdEq`rurc4$ zRm+vNqFUfvW!u-iZfvL<+C-_M)pz5(smq#CF6pKr>QYE(@)oP6in6uX)(a8Z>~d%0 zKkMW^oCvx7LMEK*?F}CESuU^hmDE~E+>48T_jcvNsVPbBTqmOQ29LG|dH%83jrgOT zga!|HH?ygb=eYfC+e-M`CLy)$W;WG!G<|z&D4go=4<6ibVY$mZ>FFlMYN5G#_Xfvj zN$b4$p~Lei;s@wtSO%(U+w{DAd-y^))z=q17<56}ryjy+Uyjy7v~%u_uwb35K6HSt z8h(62Oz_nESj_G`f8K)j>0>T5`@l`m?91U=Xm-xMp^0^rpjj)tch)P>xx7~Hk2N<` zy{k3}A3z+ty7@^8F}3e0&{Q86*e0q5lsm6_cM92+<@xD3-o$Kfj_UjC z6pP|ER8=O*IIcm9onaKK%fV!BD!VeXEUojc6Z5sdRxA~6iL-t)#SNoWTg=oe{zf-V zv)n|nvR*Pvg%a1Z#?N`y?%o1lzTIYMOpM=qE4nuxavE)!J&RuBQOf0VagT1d@ZEs3 zL^dwX8Z}%qP{ULUxAvCAcDSx_VP0o^x2(HX^+&b4NP46C zy{(`RE#_((cKy;H&q7Qg?J02gsOE?yje9;FCISno3m1ZSa-;33n-ti2(?WyEJFgm9WlPmu{~=dzFdZn& zDClaL?)o%v6K9U}ddYj6KJoYe@Aq_%`XB%TAOHd&00JNY0w4eaAOHd&@aPl3`TxQ0O$YrOAz%z00ck)1V8`;KmY_l00ck)1VG@? zCqU=_mgMIH$uILCc!2;2fB*=900@8p2!H?xfB*=900?}*1TM6+2f72NZqO$U&a$mF zri-5j|HL&-Drv<%XVba?*%0h<-wfdB}A00@8p2!H?xfB*=900@A}oeRh2a9ivMS009sH0T2KI z5C8!X009sH0T2Lz4~zgk{|_d=5a3^UfdB}A00@8p2!H?xfB*=900@8p2z-bHJ|1k_ zj}Nw8X6wVsW%=T|yf&0sA01vBRfaQ4A)~B~56PF6;kC=dj9pr1%2;7-j9nZpTpS&~ zbotWg#kI`naOTp;-maP1u54}$kB{sarWyR)kDWQcPtX5@$)61HFT6ki1V8`;KmY_l z00ck)1V8`;KmY_DJpz-V{dna4-W0(5{~tZWLL(3W0T2KI5C8!X009sH0T2KI5O~l8 z;+@|eh_qe~B!4{dUpq^Y@9g+|_)L3yh_zl0euZa!d*B-%bdUAhYrooVW!!!x8&382 z2M=B{<+UQK7))PeMya9~nBhxYn^??cmvho`_UcSd%KP&2(m*s~iKdmjWU^gTnwwvi z=2m8A&hiZB6B8*@)l9a*bZ`FD{9@7CX}M+bk?PKLqTcdogjP4*K_kG^Jua__qRKSW;$>f6 zt>tCs+=ExaYU;!EPges!G9e~p&HH{dedi-DS+G9c>cYxefnve3FSE6hB1Y$Lp3A6hC#$K3xi_DDF`OD33*K3>3}o(< zrnq(`{i3n$u`{A=}@+98Nv=T=32X3%pS&)1a7sgY9{*U-6s3 znr_N6E$Ak$nEKa(!D|Cn>CI39gSkynadq_tnnT{M_UO$MG|s=YSWWpjd>oL z-E9VbeMxA|;sVgUW^nfy#5aDuuZWFbn`=M&^kIP!2a(BIE82yGAyuoGYfA4b>NSyL z)_rBnTCnf*is@7Mllo1(baUknyC@Z9-N#Ar@Fg?-+0U#9odY&Kk6R?iGbGQGisdaF>|MuFNb; z>vGXxzP8;uOU$g47uj1C{#SVot6EAry)3bU&bT8})L6bvqkeblyn0bk#WY_bWR z@t+P*e@yAzR9R(9)!eZ+9*9451TlZ)aG4c2#Iotw>7Q&*z0}<(cE}}GGxYNYpXEAe zrn~*j)o`k}H+WF>r6nG41{Ja{uGe~%B^Rzm#mLsyOyr}%8}T@Sm!p5^b_@QolkmP~ z4Q@|=vxey3-z*6Y-maV3^sEbTUf-~sjDAnmVk7ZAh?Vq>x~`{}ij7<^ zuNw0^8qJH&YSwV3@w?MQ@=`GbKWi={%uZHQE?VRMHUodlB(y9j0Nra+aTn=mdhFHq z)O6q7rj==Jn}SQsE_Nb!r}vd`s;@8j={DEwc9z9cRGF+bzFo*(y{uxcr5kN`8hL|f zZSzh-_t@Fw&C)H#=kc0(?Jmyc_ZY{1-p$Y>V_J8wb^F{E%R&cdTnkN>_`0ZBv1wmU z)LO{SxkrR5R#UF&JnMs>)kBIdw|jpW2r&hMr$AF-7EO!y|6dIxuRgG20vZAV5C8!X z009sH0T2KI5C8!X0D&iv!2V0Y2a|sk;9q!w00@8p2!H?xfB*=900@8p2!H?x zJP8DjwhkV?If3{8KM97#7(f67KmY_l00ck)1V8`;KmY_l;O+$I`G276?q!e*0w4ea zAOHd&00JNY0w4eaAOHd&@OTrz^Z&=&e$XBSKmY_l00ck)1V8`;KmY_l00f#Lfam|s eP(m3H009sH0T2KI5C8!X009sH0T6h+3H*O|2gm3D diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts index 83dbf9d0..490f9585 100644 --- a/server/setup/scriptsSqlite/1.9.0.ts +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -10,26 +10,175 @@ export default async function migration() { const location = path.join(APP_PATH, "db", "db.sqlite"); const db = new Database(location); + const resourceSiteMap = new Map(); + + try { + const resources = db + .prepare( + "SELECT resourceId, siteId FROM resources WHERE siteId IS NOT NULL" + ) + .all() as Array<{ resourceId: number; siteId: number }>; + for (const resource of resources) { + resourceSiteMap.set(resource.resourceId, resource.siteId); + } + } catch (e) { + console.log("Error getting resources:", e); + } + try { db.pragma("foreign_keys = OFF"); db.transaction(() => { - db.exec(` - CREATE TABLE 'setupTokens' ( - 'tokenId' text PRIMARY KEY NOT NULL, - 'token' text NOT NULL, - 'used' integer DEFAULT 0 NOT NULL, - 'dateCreated' text NOT NULL, - 'dateUsed' text + db.exec(`CREATE TABLE 'setupTokens' ( + 'tokenId' text PRIMARY KEY NOT NULL, + 'token' text NOT NULL, + 'used' integer DEFAULT false NOT NULL, + 'dateCreated' text NOT NULL, + 'dateUsed' text +); +--> statement-breakpoint +CREATE TABLE 'siteResources' ( + 'siteResourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'siteId' integer NOT NULL, + 'orgId' text NOT NULL, + 'name' text NOT NULL, + 'protocol' text NOT NULL, + 'proxyPort' integer NOT NULL, + 'destinationPort' integer NOT NULL, + 'destinationIp' text NOT NULL, + 'enabled' integer DEFAULT true NOT NULL, + FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE '__new_resources' ( + 'resourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'orgId' text NOT NULL, + 'name' text NOT NULL, + 'subdomain' text, + 'fullDomain' text, + 'domainId' text, + 'ssl' integer DEFAULT false NOT NULL, + 'blockAccess' integer DEFAULT false NOT NULL, + 'sso' integer DEFAULT true NOT NULL, + 'http' integer DEFAULT true NOT NULL, + 'protocol' text NOT NULL, + 'proxyPort' integer, + 'emailWhitelistEnabled' integer DEFAULT false NOT NULL, + 'applyRules' integer DEFAULT false NOT NULL, + 'enabled' integer DEFAULT true NOT NULL, + 'stickySession' integer DEFAULT false NOT NULL, + 'tlsServerName' text, + 'setHostHeader' text, + 'enableProxy' integer DEFAULT true, + 'skipToIdpId' integer, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null, + FOREIGN KEY ('skipToIdpId') REFERENCES 'idp'('idpId') ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO '__new_resources'("resourceId", "orgId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId") SELECT "resourceId", "orgId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", null FROM 'resources';--> statement-breakpoint +DROP TABLE 'resources';--> statement-breakpoint +ALTER TABLE '__new_resources' RENAME TO 'resources';--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE TABLE '__new_clients' ( + 'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'orgId' text NOT NULL, + 'exitNode' integer, + 'name' text NOT NULL, + 'pubKey' text, + 'subnet' text NOT NULL, + 'bytesIn' integer, + 'bytesOut' integer, + 'lastBandwidthUpdate' text, + 'lastPing' integer, + 'type' text NOT NULL, + 'online' integer DEFAULT false NOT NULL, + 'lastHolePunch' integer, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('exitNode') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +INSERT INTO '__new_clients'("id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", "lastPing", "type", "online", "lastHolePunch") SELECT "id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", "lastPing", "type", "online", "lastHolePunch" FROM 'clients';--> statement-breakpoint +DROP TABLE 'clients';--> statement-breakpoint +ALTER TABLE '__new_clients' RENAME TO 'clients';--> statement-breakpoint +ALTER TABLE 'clientSites' ADD 'endpoint' text;--> statement-breakpoint +ALTER TABLE 'exitNodes' ADD 'online' integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE 'exitNodes' ADD 'lastPing' integer;--> statement-breakpoint +ALTER TABLE 'exitNodes' ADD 'type' text DEFAULT 'gerbil';--> statement-breakpoint +ALTER TABLE 'olms' ADD 'version' text;--> statement-breakpoint +ALTER TABLE 'orgs' ADD 'createdAt' text;--> statement-breakpoint +ALTER TABLE 'targets' ADD 'siteId' integer NOT NULL DEFAULT 1 REFERENCES sites(siteId);`); + + // for each resource, get all of its targets, and update the siteId to be the previously stored siteId + for (const [resourceId, siteId] of resourceSiteMap) { + const targets = db + .prepare( + "SELECT targetId FROM targets WHERE resourceId = ?" + ) + .all(resourceId) as Array<{ targetId: number }>; + for (const target of targets) { + db.prepare( + "UPDATE targets SET siteId = ? WHERE targetId = ?" + ).run(siteId, target.targetId); + } + } + + // list resources that have enableProxy false + // move them to the siteResources table + // remove them from the resources table + const proxyFalseResources = db + .prepare("SELECT * FROM resources WHERE enableProxy = 0") + .all() as Array; + + for (const resource of proxyFalseResources) { + // Get the first target to derive destination IP and port + const firstTarget = db + .prepare( + "SELECT ip, port FROM targets WHERE resourceId = ? LIMIT 1" + ) + .get(resource.resourceId) as + | { ip: string; port: number } + | undefined; + + if (!firstTarget) { + continue; + } + + // Insert into siteResources table + const stmt = db.prepare(` + INSERT INTO siteResources (siteId, orgId, name, protocol, proxyPort, destinationPort, destinationIp, enabled) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + stmt.run( + resourceSiteMap.get(resource.resourceId), + resource.orgId, + resource.name, + resource.protocol, + resource.proxyPort, + firstTarget.port, + firstTarget.ip, + resource.enabled ); - `); + + // Delete from resources table + db.prepare("DELETE FROM resources WHERE resourceId = ?").run( + resource.resourceId + ); + + // Delete the targets for this resource + db.prepare("DELETE FROM targets WHERE resourceId = ?").run( + resource.resourceId + ); + } })(); db.pragma("foreign_keys = ON"); - console.log(`Added setupTokens table`); + console.log(`Migrated database`); } catch (e) { - console.log("Unable to add setupTokens table:", e); + console.log("Failed to migrate db:", e); throw e; } } From b4be620a5bdcaa218dd04cdf1dcc602b15abba5c Mon Sep 17 00:00:00 2001 From: Pallavi Date: Tue, 19 Aug 2025 03:53:49 +0530 Subject: [PATCH 162/219] Fix: responsive layout for CardHeader (small/medium/large screens) --- .../settings/resources/ResourcesTable.tsx | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index a4209bee..07fec698 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -665,22 +665,21 @@ export default function SitesTable({ className="w-full" onValueChange={handleTabChange} > - -
- {getSearchInput()} - + +
+
{getSearchInput()}
{env.flags.enableClients && ( - - + + {t("resourcesTableProxyResources")} - + {t("resourcesTableClientResources")} )}
-
+
{getActionButton()}
@@ -700,12 +699,12 @@ export default function SitesTable({ {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} @@ -798,12 +797,12 @@ export default function SitesTable({ {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} From ffe2512734d1917cb89f53d5748857bf129e365c Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 18 Aug 2025 15:27:59 -0700 Subject: [PATCH 163/219] Update --- server/lib/traefikConfig.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/server/lib/traefikConfig.ts b/server/lib/traefikConfig.ts index d62c5f7f..a168ea0b 100644 --- a/server/lib/traefikConfig.ts +++ b/server/lib/traefikConfig.ts @@ -304,9 +304,10 @@ export class TraefikConfigManager { (1000 * 60) ) : 0; - logger.debug( - `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` - ); + + // logger.debug( + // `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` + // ); // Still need to ensure config is up to date with existing certificates await this.updateDynamicConfigFromLocalCerts(domains); @@ -554,8 +555,8 @@ export class TraefikConfigManager { const keyPath = path.join(domainDir, "key.pem"); const certEntry = { - certFile: `/var/${certPath}`, - keyFile: `/var/${keyPath}` + certFile: certPath, + keyFile: keyPath }; dynamicConfig.tls.certificates.push(certEntry); } @@ -664,8 +665,8 @@ export class TraefikConfigManager { // Always ensure the config entry exists and is up to date const certEntry = { - certFile: `/var/${certPath}`, - keyFile: `/var/${keyPath}` + certFile: certPath, + keyFile: keyPath }; // Remove any existing entry for this cert/key path dynamicConfig.tls.certificates = @@ -809,14 +810,14 @@ export class TraefikConfigManager { this.lastLocalCertificateState.delete(dirName); // Remove from dynamic config - const certFilePath = `/var/${path.join( + const certFilePath = path.join( domainDir, "cert.pem" - )}`; - const keyFilePath = `/var/${path.join( + ); + const keyFilePath = path.join( domainDir, "key.pem" - )}`; + ); const before = dynamicConfig.tls.certificates.length; dynamicConfig.tls.certificates = dynamicConfig.tls.certificates.filter( From 378de19f4142ebd8cbdd4b536e439bf410a7be0a Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 18 Aug 2025 15:29:04 -0700 Subject: [PATCH 164/219] add pg 1.9.0 migration --- .gitignore | 2 + docker-compose.pg.yml | 4 +- server/setup/scriptsPg/1.9.0.ts | 131 ++++++++++++++++++++++++++-- server/setup/scriptsSqlite/1.9.0.ts | 2 +- 4 files changed, 128 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 167b4a91..84dbd2d5 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ next-env.d.ts migrations tsconfig.tsbuildinfo config/config.yml +config/postgres +config/postgres* dist .dist installer diff --git a/docker-compose.pg.yml b/docker-compose.pg.yml index aeffc2cf..ee50d328 100644 --- a/docker-compose.pg.yml +++ b/docker-compose.pg.yml @@ -7,6 +7,8 @@ services: POSTGRES_DB: postgres # Default database name POSTGRES_USER: postgres # Default user POSTGRES_PASSWORD: password # Default password (change for production!) + volumes: + - ./config/postgres:/var/lib/postgresql/data ports: - "5432:5432" # Map host port 5432 to container port 5432 - restart: no \ No newline at end of file + restart: no diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts index a12f5617..bee521a3 100644 --- a/server/setup/scriptsPg/1.9.0.ts +++ b/server/setup/scriptsPg/1.9.0.ts @@ -3,23 +3,136 @@ import { sql } from "drizzle-orm"; const version = "1.9.0"; +await migration(); + export default async function migration() { console.log(`Running setup script ${version}...`); + const resourceSiteMap = new Map(); + let firstSiteId: number = 1; + try { - await db.execute(sql` - CREATE TABLE "setupTokens" ( - "tokenId" varchar PRIMARY KEY NOT NULL, - "token" varchar NOT NULL, - "used" boolean DEFAULT false NOT NULL, - "dateCreated" varchar NOT NULL, - "dateUsed" varchar + // Get the first siteId to use as default + const firstSite = await db.execute(sql`SELECT "siteId" FROM "sites" LIMIT 1`); + if (firstSite.rows.length > 0) { + firstSiteId = firstSite.rows[0].siteId as number; + } + + const resources = await db.execute(sql` + SELECT "resourceId", "siteId" FROM "resources" WHERE "siteId" IS NOT NULL + `); + for (const resource of resources.rows) { + resourceSiteMap.set( + resource.resourceId as number, + resource.siteId as number ); + } + } catch (e) { + console.log("Error getting resources:", e); + } + + try { + await db.execute(sql`BEGIN`); + + await db.execute(sql`CREATE TABLE "setupTokens" ( + "tokenId" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "used" boolean DEFAULT false NOT NULL, + "dateCreated" varchar NOT NULL, + "dateUsed" varchar +);`); + + await db.execute(sql`CREATE TABLE "siteResources" ( + "siteResourceId" serial PRIMARY KEY NOT NULL, + "siteId" integer NOT NULL, + "orgId" varchar NOT NULL, + "name" varchar NOT NULL, + "protocol" varchar NOT NULL, + "proxyPort" integer NOT NULL, + "destinationPort" integer NOT NULL, + "destinationIp" varchar NOT NULL, + "enabled" boolean DEFAULT true NOT NULL +);`); + + await db.execute(sql`ALTER TABLE "resources" DROP CONSTRAINT "resources_siteId_sites_siteId_fk";`); + + await db.execute(sql`ALTER TABLE "clients" ALTER COLUMN "lastPing" TYPE integer USING NULL;`); + + await db.execute(sql`ALTER TABLE "clientSites" ADD COLUMN "endpoint" varchar;`); + + await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "version" text;`); + + await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "createdAt" text;`); + + await db.execute(sql`ALTER TABLE "resources" ADD COLUMN "skipToIdpId" integer;`); + + await db.execute(sql.raw(`ALTER TABLE "targets" ADD COLUMN "siteId" integer NOT NULL DEFAULT ${firstSiteId || 1};`)); + + await db.execute(sql`ALTER TABLE "siteResources" ADD CONSTRAINT "siteResources_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "siteResources" ADD CONSTRAINT "siteResources_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "resources" ADD CONSTRAINT "resources_skipToIdpId_idp_idpId_fk" FOREIGN KEY ("skipToIdpId") REFERENCES "public"."idp"("idpId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "targets" ADD CONSTRAINT "targets_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "clients" DROP COLUMN "endpoint";`); + + await db.execute(sql`ALTER TABLE "resources" DROP COLUMN "siteId";`); + + // for each resource, get all of its targets, and update the siteId to be the previously stored siteId + for (const [resourceId, siteId] of resourceSiteMap) { + const targets = await db.execute(sql` + SELECT "targetId" FROM "targets" WHERE "resourceId" = ${resourceId} + `); + for (const target of targets.rows) { + await db.execute(sql` + UPDATE "targets" SET "siteId" = ${siteId} WHERE "targetId" = ${target.targetId} + `); + } + } + + // list resources that have enableProxy false + // move them to the siteResources table + // remove them from the resources table + const proxyFalseResources = await db.execute(sql` + SELECT * FROM "resources" WHERE "enableProxy" = false `); - console.log(`Added setupTokens table`); + for (const resource of proxyFalseResources.rows) { + // Get the first target to derive destination IP and port + const firstTarget = await db.execute(sql` + SELECT "ip", "port" FROM "targets" WHERE "resourceId" = ${resource.resourceId} LIMIT 1 + `); + + if (firstTarget.rows.length === 0) { + continue; + } + + const target = firstTarget.rows[0]; + + // Insert into siteResources table + await db.execute(sql` + INSERT INTO "siteResources" ("siteId", "orgId", "name", "protocol", "proxyPort", "destinationPort", "destinationIp", "enabled") + VALUES (${resourceSiteMap.get(resource.resourceId as number)}, ${resource.orgId}, ${resource.name}, ${resource.protocol}, ${resource.proxyPort}, ${target.port}, ${target.ip}, ${resource.enabled}) + `); + + // Delete from resources table + await db.execute(sql` + DELETE FROM "resources" WHERE "resourceId" = ${resource.resourceId} + `); + + // Delete the targets for this resource + await db.execute(sql` + DELETE FROM "targets" WHERE "resourceId" = ${resource.resourceId} + `); + } + + await db.execute(sql`COMMIT`); + console.log(`Migrated database`); } catch (e) { - console.log("Unable to add setupTokens table:", e); + await db.execute(sql`ROLLBACK`); + console.log("Failed to migrate db:", e); throw e; } } diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts index 490f9585..502d7063 100644 --- a/server/setup/scriptsSqlite/1.9.0.ts +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -100,7 +100,7 @@ CREATE TABLE '__new_clients' ( FOREIGN KEY ('exitNode') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null ); --> statement-breakpoint -INSERT INTO '__new_clients'("id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", "lastPing", "type", "online", "lastHolePunch") SELECT "id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", "lastPing", "type", "online", "lastHolePunch" FROM 'clients';--> statement-breakpoint +INSERT INTO '__new_clients'("id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", "lastPing", "type", "online", "lastHolePunch") SELECT "id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", NULL, "type", "online", "lastHolePunch" FROM 'clients';--> statement-breakpoint DROP TABLE 'clients';--> statement-breakpoint ALTER TABLE '__new_clients' RENAME TO 'clients';--> statement-breakpoint ALTER TABLE 'clientSites' ADD 'endpoint' text;--> statement-breakpoint From 49f84bccad78a73a2c07e3d90ba9287b423d91ee Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 18 Aug 2025 15:43:48 -0700 Subject: [PATCH 165/219] migrations --- server/setup/scriptsPg/1.9.0.ts | 2 -- server/setup/scriptsSqlite/1.9.0.ts | 33 +++++++++++++++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts index bee521a3..a25bd780 100644 --- a/server/setup/scriptsPg/1.9.0.ts +++ b/server/setup/scriptsPg/1.9.0.ts @@ -3,8 +3,6 @@ import { sql } from "drizzle-orm"; const version = "1.9.0"; -await migration(); - export default async function migration() { console.log(`Running setup script ${version}...`); diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts index 502d7063..5f247ea5 100644 --- a/server/setup/scriptsSqlite/1.9.0.ts +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -11,19 +11,26 @@ export default async function migration() { const db = new Database(location); const resourceSiteMap = new Map(); + let firstSiteId: number = 1; - try { - const resources = db - .prepare( - "SELECT resourceId, siteId FROM resources WHERE siteId IS NOT NULL" - ) - .all() as Array<{ resourceId: number; siteId: number }>; - for (const resource of resources) { - resourceSiteMap.set(resource.resourceId, resource.siteId); - } - } catch (e) { - console.log("Error getting resources:", e); - } + try { + // Get the first siteId to use as default + const firstSite = db.prepare("SELECT siteId FROM sites LIMIT 1").get() as { siteId: number } | undefined; + if (firstSite) { + firstSiteId = firstSite.siteId; + } + + const resources = db + .prepare( + "SELECT resourceId, siteId FROM resources WHERE siteId IS NOT NULL" + ) + .all() as Array<{ resourceId: number; siteId: number }>; + for (const resource of resources) { + resourceSiteMap.set(resource.resourceId, resource.siteId); + } + } catch (e) { + console.log("Error getting resources:", e); + } try { db.pragma("foreign_keys = OFF"); @@ -109,7 +116,7 @@ ALTER TABLE 'exitNodes' ADD 'lastPing' integer;--> statement-breakpoint ALTER TABLE 'exitNodes' ADD 'type' text DEFAULT 'gerbil';--> statement-breakpoint ALTER TABLE 'olms' ADD 'version' text;--> statement-breakpoint ALTER TABLE 'orgs' ADD 'createdAt' text;--> statement-breakpoint -ALTER TABLE 'targets' ADD 'siteId' integer NOT NULL DEFAULT 1 REFERENCES sites(siteId);`); +ALTER TABLE 'targets' ADD 'siteId' integer NOT NULL DEFAULT ${firstSiteId || 1} REFERENCES sites(siteId);`); // for each resource, get all of its targets, and update the siteId to be the previously stored siteId for (const [resourceId, siteId] of resourceSiteMap) { From 34002470a5c9e48c85c9af6f95986564dfa0325f Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 18 Aug 2025 16:27:51 -0700 Subject: [PATCH 166/219] add migration to scirpts --- server/setup/migrationsPg.ts | 2 +- server/setup/migrationsSqlite.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index fd9a7c21..6b3f20b9 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -18,7 +18,7 @@ const migrations = [ { version: "1.6.0", run: m1 }, { version: "1.7.0", run: m2 }, { version: "1.8.0", run: m3 }, - // { version: "1.9.0", run: m4 } + { version: "1.9.0", run: m4 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 5411261f..5b0850c8 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -50,7 +50,7 @@ const migrations = [ { version: "1.6.0", run: m21 }, { version: "1.7.0", run: m22 }, { version: "1.8.0", run: m23 }, - // { version: "1.9.0", run: m24 }, + { version: "1.9.0", run: m24 }, // Add new migrations here as they are created ] as const; From 21743e5a2382eb01213f675e50e23faa36fbc2c8 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 19 Aug 2025 10:25:02 -0700 Subject: [PATCH 167/219] Clarify site address --- messages/en-US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/en-US.json b/messages/en-US.json index 602754d8..ba22ff77 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1438,7 +1438,7 @@ "siteAcceptClientConnections": "Accept Client Connections", "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to.", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", "autoLoginExternalIdp": "Auto Login with External IDP", "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", "selectIdp": "Select IDP", From 254b3a0fc84dd82ef3ac84c95eda3ce5f8ea35b3 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 19 Aug 2025 11:26:18 -0700 Subject: [PATCH 168/219] Also filer out offline sites? --- server/routers/traefik/getTraefikConfig.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 918df3fd..2653be67 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -131,6 +131,7 @@ export async function getTraefikConfig( // Site fields siteId: sites.siteId, siteType: sites.type, + siteOnline: sites.online, subnet: sites.subnet, exitNodeId: sites.exitNodeId }) @@ -141,6 +142,7 @@ export async function getTraefikConfig( and( eq(targets.enabled, true), eq(resources.enabled, true), + eq(sites.online, true), or( eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId) From a0d6646e49288f9877c21db44142676a4add2ded Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:01 -0700 Subject: [PATCH 169/219] New translations en-us.json (French) --- messages/fr-FR.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 16e286d9..0eeb7b85 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.", "siteWg": "WireGuard basique", "siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Ressources locales seulement. Pas de tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Voir tous les sites", "siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site", "siteNewtCredentials": "Identifiants Newt", @@ -166,7 +168,7 @@ "siteSelect": "Sélectionner un site", "siteSearch": "Chercher un site", "siteNotFound": "Aucun site trouvé.", - "siteSelectionDescription": "Ce site fournira la connectivité à la ressource.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Type de ressource", "resourceTypeDescription": "Déterminer comment vous voulez accéder à votre ressource", "resourceHTTPSSettings": "Paramètres HTTPS", @@ -197,6 +199,7 @@ "general": "Généraux", "generalSettings": "Paramètres généraux", "proxy": "Proxy", + "internal": "Internal", "rules": "Règles", "resourceSettingDescription": "Configurer les paramètres de votre ressource", "resourceSetting": "Réglages {resourceName}", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.", "targetTlsSubmit": "Enregistrer les paramètres", "targets": "Configuration des cibles", - "targetsDescription": "Configurez les cibles pour router le trafic vers vos services", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Activer les sessions persistantes", "targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.", "methodSelect": "Sélectionner la méthode", @@ -970,6 +973,7 @@ "logoutError": "Erreur lors de la déconnexion", "signingAs": "Connecté en tant que", "serverAdmin": "Admin Serveur", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Activer l'authentification à deux facteurs", "otpDisable": "Désactiver l'authentification à deux facteurs", "logout": "Déconnexion", @@ -986,7 +990,7 @@ "actionGetSite": "Obtenir un site", "actionListSites": "Lister les sites", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Mettre à jour un site", "actionListSiteRoles": "Lister les rôles autorisés du site", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Activer le proxy public", "resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.", - "externalProxyEnabled": "Proxy externe activé" -} \ No newline at end of file + "externalProxyEnabled": "Proxy externe activé", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 0383ffb7f33f2fcd42fee608a6b4b8237d230cd3 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:02 -0700 Subject: [PATCH 170/219] New translations en-us.json (Spanish) --- messages/es-ES.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index 3f862cea..22981175 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en tu red. Sin configuración adicional.", "siteWg": "Wirex Guardia Básica", "siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Solo recursos locales. Sin túneles.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Ver todos los sitios", "siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio", "siteNewtCredentials": "Credenciales nuevas", @@ -166,7 +168,7 @@ "siteSelect": "Seleccionar sitio", "siteSearch": "Buscar sitio", "siteNotFound": "Sitio no encontrado.", - "siteSelectionDescription": "Este sitio proporcionará conectividad al recurso.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Tipo de recurso", "resourceTypeDescription": "Determina cómo quieres acceder a tu recurso", "resourceHTTPSSettings": "Configuración HTTPS", @@ -197,6 +199,7 @@ "general": "General", "generalSettings": "Configuración General", "proxy": "Proxy", + "internal": "Internal", "rules": "Reglas", "resourceSettingDescription": "Configure la configuración de su recurso", "resourceSetting": "Ajustes {resourceName}", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.", "targetTlsSubmit": "Guardar ajustes", "targets": "Configuración de objetivos", - "targetsDescription": "Configurar objetivos para enrutar tráfico a sus servicios", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Activar Sesiones Pegadas", "targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.", "methodSelect": "Seleccionar método", @@ -970,6 +973,7 @@ "logoutError": "Error al cerrar sesión", "signingAs": "Conectado como", "serverAdmin": "Admin Servidor", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Activar doble factor", "otpDisable": "Desactivar doble factor", "logout": "Cerrar sesión", @@ -986,7 +990,7 @@ "actionGetSite": "Obtener sitio", "actionListSites": "Listar sitios", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Actualizar sitio", "actionListSiteRoles": "Lista de roles permitidos del sitio", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Habilitar proxy público", "resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.", - "externalProxyEnabled": "Proxy externo habilitado" -} \ No newline at end of file + "externalProxyEnabled": "Proxy externo habilitado", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 1665bf651518c667a71bc8ba894c7a744dc38372 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:03 -0700 Subject: [PATCH 171/219] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 1d982bc6..ecbc65ca 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", "siteWg": "Basic WireGuard", "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Local resources only. No tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "See All Sites", "siteTunnelDescription": "Determine how you want to connect to your site", "siteNewtCredentials": "Newt Credentials", @@ -166,7 +168,7 @@ "siteSelect": "Select site", "siteSearch": "Search site", "siteNotFound": "No site found.", - "siteSelectionDescription": "This site will provide connectivity to the resource.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Resource Type", "resourceTypeDescription": "Determine how you want to access your resource", "resourceHTTPSSettings": "HTTPS Settings", @@ -197,6 +199,7 @@ "general": "General", "generalSettings": "General Settings", "proxy": "Proxy", + "internal": "Internal", "rules": "Rules", "resourceSettingDescription": "Configure the settings on your resource", "resourceSetting": "{resourceName} Settings", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSubmit": "Save Settings", "targets": "Targets Configuration", - "targetsDescription": "Set up targets to route traffic to your services", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Enable Sticky Sessions", "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "methodSelect": "Select method", @@ -970,6 +973,7 @@ "logoutError": "Error logging out", "signingAs": "Signed in as", "serverAdmin": "Server Admin", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Enable Two-factor", "otpDisable": "Disable Two-factor", "logout": "Log Out", @@ -986,7 +990,7 @@ "actionGetSite": "Get Site", "actionListSites": "List Sites", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" -} \ No newline at end of file + "externalProxyEnabled": "External Proxy Enabled", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From eae3ab2dc1f1a03d8f554d55ff49b30533b6298a Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:04 -0700 Subject: [PATCH 172/219] New translations en-us.json (Czech) --- messages/cs-CZ.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index d21f37c2..7c4fd609 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", "siteWg": "Basic WireGuard", "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Local resources only. No tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "See All Sites", "siteTunnelDescription": "Determine how you want to connect to your site", "siteNewtCredentials": "Newt Credentials", @@ -166,7 +168,7 @@ "siteSelect": "Select site", "siteSearch": "Search site", "siteNotFound": "No site found.", - "siteSelectionDescription": "This site will provide connectivity to the resource.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Resource Type", "resourceTypeDescription": "Determine how you want to access your resource", "resourceHTTPSSettings": "HTTPS Settings", @@ -197,6 +199,7 @@ "general": "General", "generalSettings": "General Settings", "proxy": "Proxy", + "internal": "Internal", "rules": "Rules", "resourceSettingDescription": "Configure the settings on your resource", "resourceSetting": "{resourceName} Settings", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSubmit": "Save Settings", "targets": "Targets Configuration", - "targetsDescription": "Set up targets to route traffic to your services", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Enable Sticky Sessions", "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "methodSelect": "Select method", @@ -970,6 +973,7 @@ "logoutError": "Error logging out", "signingAs": "Signed in as", "serverAdmin": "Server Admin", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Enable Two-factor", "otpDisable": "Disable Two-factor", "logout": "Log Out", @@ -986,7 +990,7 @@ "actionGetSite": "Get Site", "actionListSites": "List Sites", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" -} \ No newline at end of file + "externalProxyEnabled": "External Proxy Enabled", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 37bfc07ffba8f95b954c51ae1600930efd1c219d Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:06 -0700 Subject: [PATCH 173/219] New translations en-us.json (German) --- messages/de-DE.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index fab7e28a..68123500 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.", "siteWg": "Einfacher WireGuard Tunnel", "siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Alle Standorte anzeigen", "siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest", "siteNewtCredentials": "Neue Newt Zugangsdaten", @@ -166,7 +168,7 @@ "siteSelect": "Standort auswählen", "siteSearch": "Standorte durchsuchen", "siteNotFound": "Keinen Standort gefunden.", - "siteSelectionDescription": "Dieser Standort wird die Verbindung zu der Ressource herstellen.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Ressourcentyp", "resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten", "resourceHTTPSSettings": "HTTPS-Einstellungen", @@ -197,6 +199,7 @@ "general": "Allgemein", "generalSettings": "Allgemeine Einstellungen", "proxy": "Proxy", + "internal": "Internal", "rules": "Regeln", "resourceSettingDescription": "Konfigurieren Sie die Einstellungen Ihrer Ressource", "resourceSetting": "{resourceName} Einstellungen", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.", "targetTlsSubmit": "Einstellungen speichern", "targets": "Ziel-Konfiguration", - "targetsDescription": "Richten Sie Ziele ein, um Datenverkehr zu Ihren Diensten zu leiten", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Sticky Sessions aktivieren", "targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.", "methodSelect": "Methode auswählen", @@ -970,6 +973,7 @@ "logoutError": "Fehler beim Abmelden", "signingAs": "Angemeldet als", "serverAdmin": "Server-Administrator", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Zwei-Faktor aktivieren", "otpDisable": "Zwei-Faktor deaktivieren", "logout": "Abmelden", @@ -986,7 +990,7 @@ "actionGetSite": "Standort abrufen", "actionListSites": "Standorte auflisten", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Standorte aktualisieren", "actionListSiteRoles": "Erlaubte Standort-Rollen auflisten", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Öffentlichen Proxy aktivieren", "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", - "externalProxyEnabled": "Externer Proxy aktiviert" -} \ No newline at end of file + "externalProxyEnabled": "Externer Proxy aktiviert", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 0a05bdba1db055990897a95a04b6c24da1430dd5 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:07 -0700 Subject: [PATCH 174/219] New translations en-us.json (Italian) --- messages/it-IT.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index 82753fc7..c21797d0 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint nella rete. Nessuna configurazione aggiuntiva.", "siteWg": "WireGuard Base", "siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Solo risorse locali. Nessun tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Vedi Tutti I Siti", "siteTunnelDescription": "Determina come vuoi connetterti al tuo sito", "siteNewtCredentials": "Credenziali Newt", @@ -166,7 +168,7 @@ "siteSelect": "Seleziona sito", "siteSearch": "Cerca sito", "siteNotFound": "Nessun sito trovato.", - "siteSelectionDescription": "Questo sito fornirà connettività alla risorsa.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Tipo Di Risorsa", "resourceTypeDescription": "Determina come vuoi accedere alla tua risorsa", "resourceHTTPSSettings": "Impostazioni HTTPS", @@ -197,6 +199,7 @@ "general": "Generale", "generalSettings": "Impostazioni Generali", "proxy": "Proxy", + "internal": "Internal", "rules": "Regole", "resourceSettingDescription": "Configura le impostazioni sulla tua risorsa", "resourceSetting": "Impostazioni {resourceName}", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.", "targetTlsSubmit": "Salva Impostazioni", "targets": "Configurazione Target", - "targetsDescription": "Configura i target per instradare il traffico ai tuoi servizi", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Abilita Sessioni Persistenti", "targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.", "methodSelect": "Seleziona metodo", @@ -970,6 +973,7 @@ "logoutError": "Errore durante il logout", "signingAs": "Accesso come", "serverAdmin": "Amministratore Server", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Abilita Autenticazione a Due Fattori", "otpDisable": "Disabilita Autenticazione a Due Fattori", "logout": "Disconnetti", @@ -986,7 +990,7 @@ "actionGetSite": "Ottieni Sito", "actionListSites": "Elenca Siti", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Aggiorna Sito", "actionListSiteRoles": "Elenca Ruoli Sito Consentiti", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Abilita Proxy Pubblico", "resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.", - "externalProxyEnabled": "Proxy Esterno Abilitato" -} \ No newline at end of file + "externalProxyEnabled": "Proxy Esterno Abilitato", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 1a098eecf63fe918f913f0a87b7e8a9f69c2f349 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:08 -0700 Subject: [PATCH 175/219] New translations en-us.json (Korean) --- messages/ko-KR.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 4e6fb851..5fa81338 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.", "siteWg": "기본 WireGuard", "siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "모든 사이트 보기", "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요", "siteNewtCredentials": "Newt 자격 증명", @@ -166,7 +168,7 @@ "siteSelect": "사이트 선택", "siteSearch": "사이트 검색", "siteNotFound": "사이트를 찾을 수 없습니다.", - "siteSelectionDescription": "이 사이트는 리소스에 대한 연결을 제공합니다.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "리소스 유형", "resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요", "resourceHTTPSSettings": "HTTPS 설정", @@ -197,6 +199,7 @@ "general": "일반", "generalSettings": "일반 설정", "proxy": "프록시", + "internal": "Internal", "rules": "규칙", "resourceSettingDescription": "리소스의 설정을 구성하세요.", "resourceSetting": "{resourceName} 설정", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.", "targetTlsSubmit": "설정 저장", "targets": "대상 구성", - "targetsDescription": "서비스로 트래픽을 라우팅할 대상을 설정하십시오", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "스티키 세션 활성화", "targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.", "methodSelect": "선택 방법", @@ -970,6 +973,7 @@ "logoutError": "로그아웃 중 오류 발생", "signingAs": "로그인한 사용자", "serverAdmin": "서버 관리자", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "이중 인증 활성화", "otpDisable": "이중 인증 비활성화", "logout": "로그 아웃", @@ -986,7 +990,7 @@ "actionGetSite": "사이트 가져오기", "actionListSites": "사이트 목록", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "사이트 업데이트", "actionListSiteRoles": "허용된 사이트 역할 목록", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" -} \ No newline at end of file + "externalProxyEnabled": "External Proxy Enabled", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 291c7aaf0b1bc83cc1385ab918346c69856bdd22 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:10 -0700 Subject: [PATCH 176/219] New translations en-us.json (Dutch) --- messages/nl-NL.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index aa8859cf..06d2f121 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.", "siteWg": "Basis WireGuard", "siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Alle werkruimtes bekijken", "siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site", "siteNewtCredentials": "Nieuwste aanmeldgegevens", @@ -166,7 +168,7 @@ "siteSelect": "Selecteer site", "siteSearch": "Zoek site", "siteNotFound": "Geen site gevonden.", - "siteSelectionDescription": "Deze site zal connectiviteit met de bron geven.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Type bron", "resourceTypeDescription": "Bepaal hoe u toegang wilt krijgen tot uw bron", "resourceHTTPSSettings": "HTTPS instellingen", @@ -197,6 +199,7 @@ "general": "Algemeen", "generalSettings": "Algemene instellingen", "proxy": "Proxy", + "internal": "Internal", "rules": "Regels", "resourceSettingDescription": "Configureer de instellingen op uw bron", "resourceSetting": "{resourceName} instellingen", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.", "targetTlsSubmit": "Instellingen opslaan", "targets": "Doelstellingen configuratie", - "targetsDescription": "Stel doelen in om verkeer naar uw diensten te leiden", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Sticky sessies inschakelen", "targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.", "methodSelect": "Selecteer methode", @@ -970,6 +973,7 @@ "logoutError": "Fout bij uitloggen", "signingAs": "Ingelogd als", "serverAdmin": "Server Beheerder", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Twee-factor inschakelen", "otpDisable": "Tweestapsverificatie uitschakelen", "logout": "Log uit", @@ -986,7 +990,7 @@ "actionGetSite": "Site ophalen", "actionListSites": "Sites weergeven", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Site bijwerken", "actionListSiteRoles": "Toon toegestane sitenollen", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Openbare proxy inschakelen", "resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.", - "externalProxyEnabled": "Externe Proxy Ingeschakeld" -} \ No newline at end of file + "externalProxyEnabled": "Externe Proxy Ingeschakeld", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 67580a8b69f11de0ada26262ba31c247b8476f8b Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:11 -0700 Subject: [PATCH 177/219] New translations en-us.json (Norwegian Bokmal) --- messages/nb-NO.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/nb-NO.json b/messages/nb-NO.json index f2b0924b..02ef58b9 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.", "siteWg": "Grunnleggende WireGuard", "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Se alle områder", "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", "siteNewtCredentials": "Newt påloggingsinformasjon", @@ -166,7 +168,7 @@ "siteSelect": "Velg område", "siteSearch": "Søk i område", "siteNotFound": "Ingen område funnet.", - "siteSelectionDescription": "Dette området vil gi tilkobling til ressursen.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Ressurstype", "resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din", "resourceHTTPSSettings": "HTTPS-innstillinger", @@ -197,6 +199,7 @@ "general": "Generelt", "generalSettings": "Generelle innstillinger", "proxy": "Proxy", + "internal": "Internal", "rules": "Regler", "resourceSettingDescription": "Konfigurer innstillingene på ressursen din", "resourceSetting": "{resourceName} Innstillinger", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", "targetTlsSubmit": "Lagre innstillinger", "targets": "Målkonfigurasjon", - "targetsDescription": "Sett opp mål for å rute trafikk til tjenestene dine", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Aktiver klebrige sesjoner", "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", "methodSelect": "Velg metode", @@ -970,6 +973,7 @@ "logoutError": "Feil ved utlogging", "signingAs": "Logget inn som", "serverAdmin": "Serveradministrator", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Aktiver tofaktor", "otpDisable": "Deaktiver tofaktor", "logout": "Logg ut", @@ -986,7 +990,7 @@ "actionGetSite": "Hent område", "actionListSites": "List opp områder", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Oppdater område", "actionListSiteRoles": "List opp tillatte områderoller", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Legg til CIDR-områder som kan få fjerntilgang til dette området. Bruk format som 10.0.0.0/24 eller 192.168.1.0/24.", "resourceEnableProxy": "Aktiver offentlig proxy", "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", - "externalProxyEnabled": "Ekstern proxy aktivert" -} \ No newline at end of file + "externalProxyEnabled": "Ekstern proxy aktivert", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From b3083ae77970679632d8af728090303634547465 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:14 -0700 Subject: [PATCH 178/219] New translations en-us.json (Polish) --- messages/pl-PL.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index edf39a6a..ab5aff78 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Łatwiejszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.", "siteWg": "Podstawowy WireGuard", "siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Zobacz wszystkie witryny", "siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną", "siteNewtCredentials": "Aktualne dane logowania", @@ -166,7 +168,7 @@ "siteSelect": "Wybierz witrynę", "siteSearch": "Szukaj witryny", "siteNotFound": "Nie znaleziono witryny.", - "siteSelectionDescription": "Ta strona zapewni połączenie z zasobem.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Typ zasobu", "resourceTypeDescription": "Określ jak chcesz uzyskać dostęp do swojego zasobu", "resourceHTTPSSettings": "Ustawienia HTTPS", @@ -197,6 +199,7 @@ "general": "Ogólny", "generalSettings": "Ustawienia ogólne", "proxy": "Serwer pośredniczący", + "internal": "Internal", "rules": "Regulamin", "resourceSettingDescription": "Skonfiguruj ustawienia zasobu", "resourceSetting": "Ustawienia {resourceName}", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.", "targetTlsSubmit": "Zapisz ustawienia", "targets": "Konfiguracja celów", - "targetsDescription": "Skonfiguruj cele do kierowania ruchu do swoich usług", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Włącz sesje trwałe", "targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.", "methodSelect": "Wybierz metodę", @@ -970,6 +973,7 @@ "logoutError": "Błąd podczas wylogowywania", "signingAs": "Zalogowany jako", "serverAdmin": "Administrator serwera", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Włącz uwierzytelnianie dwuskładnikowe", "otpDisable": "Wyłącz uwierzytelnianie dwuskładnikowe", "logout": "Wyloguj się", @@ -986,7 +990,7 @@ "actionGetSite": "Pobierz witrynę", "actionListSites": "Lista witryn", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Aktualizuj witrynę", "actionListSiteRoles": "Lista dozwolonych ról witryny", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Włącz publiczny proxy", "resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.", - "externalProxyEnabled": "Zewnętrzny Proxy Włączony" -} \ No newline at end of file + "externalProxyEnabled": "Zewnętrzny Proxy Włączony", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 747979f939eba3c6bd6bf33f70527477f790a578 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:15 -0700 Subject: [PATCH 179/219] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index ad32ce79..96ac821e 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "A maneira mais fácil de criar um ponto de entrada na sua rede. Nenhuma configuração extra.", "siteWg": "WireGuard Básico", "siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Recursos locais apenas. Sem túneis.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Ver todos os sites", "siteTunnelDescription": "Determine como você deseja se conectar ao seu site", "siteNewtCredentials": "Credenciais Novas", @@ -166,7 +168,7 @@ "siteSelect": "Selecionar site", "siteSearch": "Procurar no site", "siteNotFound": "Nenhum site encontrado.", - "siteSelectionDescription": "Este site fornecerá conectividade ao recurso.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Tipo de Recurso", "resourceTypeDescription": "Determine como você deseja acessar seu recurso", "resourceHTTPSSettings": "Configurações de HTTPS", @@ -197,6 +199,7 @@ "general": "Gerais", "generalSettings": "Configurações Gerais", "proxy": "Proxy", + "internal": "Internal", "rules": "Regras", "resourceSettingDescription": "Configure as configurações do seu recurso", "resourceSetting": "Configurações do {resourceName}", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.", "targetTlsSubmit": "Salvar Configurações", "targets": "Configuração de Alvos", - "targetsDescription": "Configure alvos para rotear tráfego para seus serviços", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Ativar Sessões Persistentes", "targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.", "methodSelect": "Selecionar método", @@ -970,6 +973,7 @@ "logoutError": "Erro ao terminar sessão", "signingAs": "Sessão iniciada como", "serverAdmin": "Administrador do Servidor", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Ativar Autenticação de Dois Fatores", "otpDisable": "Desativar Autenticação de Dois Fatores", "logout": "Terminar Sessão", @@ -986,7 +990,7 @@ "actionGetSite": "Obter Site", "actionListSites": "Listar Sites", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Atualizar Site", "actionListSiteRoles": "Listar Funções Permitidas do Site", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Ativar Proxy Público", "resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.", - "externalProxyEnabled": "Proxy Externo Habilitado" -} \ No newline at end of file + "externalProxyEnabled": "Proxy Externo Habilitado", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 44f8098e4abeeaba5e057a70761055553a409ec0 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:16 -0700 Subject: [PATCH 180/219] New translations en-us.json (Russian) --- messages/ru-RU.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index f9a49a3f..e2cc26e2 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Простейший способ создать точку входа в вашу сеть. Дополнительная настройка не требуется.", "siteWg": "Базовый WireGuard", "siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Только локальные ресурсы. Без туннелирования.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Просмотреть все сайты", "siteTunnelDescription": "Выберите способ подключения к вашему сайту", "siteNewtCredentials": "Учётные данные Newt", @@ -166,7 +168,7 @@ "siteSelect": "Выберите сайт", "siteSearch": "Поиск сайта", "siteNotFound": "Сайт не найден.", - "siteSelectionDescription": "Этот сайт обеспечит подключение к ресурсу.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Тип ресурса", "resourceTypeDescription": "Определите, как вы хотите получать доступ к вашему ресурсу", "resourceHTTPSSettings": "Настройки HTTPS", @@ -197,6 +199,7 @@ "general": "Общие", "generalSettings": "Общие настройки", "proxy": "Прокси", + "internal": "Internal", "rules": "Правила", "resourceSettingDescription": "Настройте параметры вашего ресурса", "resourceSetting": "Настройки {resourceName}", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.", "targetTlsSubmit": "Сохранить настройки", "targets": "Конфигурация целей", - "targetsDescription": "Настройте цели для маршрутизации трафика к вашим сервисам", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Включить фиксированные сессии", "targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.", "methodSelect": "Выберите метод", @@ -970,6 +973,7 @@ "logoutError": "Ошибка при выходе", "signingAs": "Вы вошли как", "serverAdmin": "Администратор сервера", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "Включить Двухфакторную Аутентификацию", "otpDisable": "Отключить двухфакторную аутентификацию", "logout": "Выйти", @@ -986,7 +990,7 @@ "actionGetSite": "Получить сайт", "actionListSites": "Список сайтов", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Обновить сайт", "actionListSiteRoles": "Список разрешенных ролей сайта", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" -} \ No newline at end of file + "externalProxyEnabled": "External Proxy Enabled", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From f60599abd329bc7d50ca10df0ae8746acbebf482 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:18 -0700 Subject: [PATCH 181/219] New translations en-us.json (Turkish) --- messages/tr-TR.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 103a94a5..6ab3d7ba 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.", "siteWg": "Temel WireGuard", "siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "Tüm Siteleri Gör", "siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin", "siteNewtCredentials": "Newt Kimlik Bilgileri", @@ -166,7 +168,7 @@ "siteSelect": "Site seç", "siteSearch": "Site ara", "siteNotFound": "Herhangi bir site bulunamadı.", - "siteSelectionDescription": "Bu site, kaynağa bağlanabilirliği sağlayacaktır.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Kaynak Türü", "resourceTypeDescription": "Kaynağınıza nasıl erişmek istediğinizi belirleyin", "resourceHTTPSSettings": "HTTPS Ayarları", @@ -197,6 +199,7 @@ "general": "Genel", "generalSettings": "Genel Ayarlar", "proxy": "Vekil Sunucu", + "internal": "Internal", "rules": "Kurallar", "resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın", "resourceSetting": "{resourceName} Ayarları", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'", "targetTlsSubmit": "Ayarları Kaydet", "targets": "Hedefler Konfigürasyonu", - "targetsDescription": "Trafiği hizmetlerinize yönlendirmek için hedefleri ayarlayın", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Yapışkan Oturumları Etkinleştir", "targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.", "methodSelect": "Yöntemi Seç", @@ -970,6 +973,7 @@ "logoutError": "Çıkış yaparken hata", "signingAs": "Olarak giriş yapıldı", "serverAdmin": "Sunucu Yöneticisi", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "İki faktörlü özelliğini etkinleştir", "otpDisable": "İki faktörlü özelliğini devre dışı bırak", "logout": "Çıkış Yap", @@ -986,7 +990,7 @@ "actionGetSite": "Siteyi Al", "actionListSites": "Siteleri Listele", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Siteyi Güncelle", "actionListSiteRoles": "İzin Verilen Site Rolleri Listele", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Genel Proxy'i Etkinleştir", "resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.", - "externalProxyEnabled": "Dış Proxy Etkinleştirildi" -} \ No newline at end of file + "externalProxyEnabled": "Dış Proxy Etkinleştirildi", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From dceb3986957b6cd743223a742b0a395fadc5c8aa Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 19 Aug 2025 12:13:19 -0700 Subject: [PATCH 182/219] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 116 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index b7b29307..c42826ca 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -94,7 +94,9 @@ "siteNewtTunnelDescription": "最简单的方式来连接到您的网络。不需要任何额外设置。", "siteWg": "基本 WireGuard", "siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "仅限本地资源。不需要隧道。", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteSeeAll": "查看所有站点", "siteTunnelDescription": "确定如何连接到您的网站", "siteNewtCredentials": "Newt 凭据", @@ -166,7 +168,7 @@ "siteSelect": "选择站点", "siteSearch": "搜索站点", "siteNotFound": "未找到站点。", - "siteSelectionDescription": "此站点将为资源提供连接。", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "资源类型", "resourceTypeDescription": "确定如何访问您的资源", "resourceHTTPSSettings": "HTTPS 设置", @@ -197,6 +199,7 @@ "general": "概览", "generalSettings": "常规设置", "proxy": "代理服务器", + "internal": "Internal", "rules": "规则", "resourceSettingDescription": "配置您资源上的设置", "resourceSetting": "{resourceName} 设置", @@ -490,7 +493,7 @@ "targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。", "targetTlsSubmit": "保存设置", "targets": "目标配置", - "targetsDescription": "设置目标来路由流量到您的服务", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "启用置顶会话", "targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。", "methodSelect": "选择方法", @@ -970,6 +973,7 @@ "logoutError": "注销错误", "signingAs": "登录为", "serverAdmin": "服务器管理员", + "managedSelfhosted": "Managed Self-Hosted", "otpEnable": "启用双因子认证", "otpDisable": "禁用双因子认证", "logout": "登出", @@ -986,7 +990,7 @@ "actionGetSite": "获取站点", "actionListSites": "站点列表", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "更新站点", "actionListSiteRoles": "允许站点角色列表", @@ -1344,5 +1348,107 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "启用公共代理", "resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。", - "externalProxyEnabled": "外部代理已启用" -} \ No newline at end of file + "externalProxyEnabled": "外部代理已启用", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept Client Connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} From 25cef26251f3c3fa129960c41b3e51b9bd0190f2 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 19 Aug 2025 21:29:56 -0700 Subject: [PATCH 183/219] Fix ws reconnect and change create site --- server/hybridServer.ts | 6 +++- server/routers/site/createSite.ts | 6 ++-- server/routers/ws/client.ts | 35 +++++++++++++++---- .../settings/sites/[niceId]/SiteInfoCard.tsx | 2 +- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/server/hybridServer.ts b/server/hybridServer.ts index e38ca088..2cd04e0d 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -139,9 +139,13 @@ export async function createHybridClientServer() { logger.error("Failed to connect:", error); } - client.sendMessageInterval( + // Store the ping interval stop function for cleanup if needed + const stopPingInterval = client.sendMessageInterval( "remoteExitNode/ping", { timestamp: Date.now() / 1000 }, 60000 ); // send every minute + + // Return client and cleanup function for potential use + return { client, stopPingInterval }; } diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 66af0b1f..3a4dd885 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -207,7 +207,7 @@ export async function createSite( await db.transaction(async (trx) => { let newSite: Site; - if (exitNodeId) { + if ((type == "wireguard" || type == "newt") && exitNodeId) { // we are creating a site with an exit node (tunneled) if (!subnet) { return next( @@ -264,12 +264,14 @@ export async function createSite( [newSite] = await trx .insert(sites) .values({ + exitNodeId: exitNodeId, orgId, name, niceId, address: updatedAddress || null, type, - dockerSocketEnabled: type == "newt", + dockerSocketEnabled: false, + online: true, subnet: "0.0.0.0/0" }) .returning(); diff --git a/server/routers/ws/client.ts b/server/routers/ws/client.ts index fda1e62c..13b5d0da 100644 --- a/server/routers/ws/client.ts +++ b/server/routers/ws/client.ts @@ -145,11 +145,11 @@ export class WebSocketClient extends EventEmitter { } private async connectWithRetry(): Promise { - if (this.isConnecting) return; + if (this.isConnecting || this.isConnected) return; this.isConnecting = true; - while (this.shouldReconnect && !this.isConnected) { + while (this.shouldReconnect && !this.isConnected && this.isConnecting) { try { await this.establishConnection(); this.isConnecting = false; @@ -157,7 +157,7 @@ export class WebSocketClient extends EventEmitter { } catch (error) { logger.error(`Failed to connect: ${error}. Retrying in ${this.reconnectInterval}ms...`); - if (!this.shouldReconnect) { + if (!this.shouldReconnect || !this.isConnecting) { this.isConnecting = false; return; } @@ -172,6 +172,13 @@ export class WebSocketClient extends EventEmitter { } private async establishConnection(): Promise { + // Clean up any existing connection before establishing a new one + if (this.conn) { + this.conn.removeAllListeners(); + this.conn.close(); + this.conn = null; + } + // Parse the base URL to determine protocol and hostname const baseURL = new URL(this.baseURL); const wsProtocol = baseURL.protocol === 'https:' ? 'wss' : 'ws'; @@ -217,9 +224,8 @@ export class WebSocketClient extends EventEmitter { if (this.conn === null) { // Connection failed during establishment reject(error); - } else { - this.handleDisconnect(); } + // Don't call handleDisconnect here as the 'close' event will handle it }); conn.on('pong', () => { @@ -232,6 +238,12 @@ export class WebSocketClient extends EventEmitter { } private startPingMonitor(): void { + // Clear any existing ping timer to prevent duplicates + if (this.pingTimer) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + this.pingTimer = setInterval(() => { if (this.conn && this.conn.readyState === WebSocket.OPEN) { this.conn.ping(); @@ -246,6 +258,11 @@ export class WebSocketClient extends EventEmitter { } private handleDisconnect(): void { + // Prevent multiple disconnect handlers from running simultaneously + if (!this.isConnected && !this.isConnecting) { + return; + } + this.setConnected(false); this.isConnecting = false; @@ -259,6 +276,12 @@ export class WebSocketClient extends EventEmitter { this.pingTimeoutTimer = null; } + // Clear any existing reconnect timer to prevent multiple reconnection attempts + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.conn) { this.conn.removeAllListeners(); this.conn = null; @@ -269,7 +292,7 @@ export class WebSocketClient extends EventEmitter { // Reconnect if needed if (this.shouldReconnect) { // Add a small delay before starting reconnection to prevent immediate retry - setTimeout(() => { + this.reconnectTimer = setTimeout(() => { this.connectWithRetry(); }, 1000); } diff --git a/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx index 36ab1727..5eed91c5 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx @@ -66,7 +66,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { - {env.flags.enableClients && ( + {env.flags.enableClients && site.type == "newt" && ( Address From 7de8bb00e7ed97b9036c7ca82f442fb3db9edc20 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 19 Aug 2025 22:07:52 -0700 Subject: [PATCH 184/219] Use the sites if they are offline for now --- server/routers/traefik/getTraefikConfig.ts | 178 ++++++++++++--------- 1 file changed, 106 insertions(+), 72 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 2653be67..311542e1 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -98,6 +98,7 @@ export async function getTraefikConfig( type: string; subnet: string | null; exitNodeId: number | null; + online: boolean; }; }; @@ -142,7 +143,6 @@ export async function getTraefikConfig( and( eq(targets.enabled, true), eq(resources.enabled, true), - eq(sites.online, true), or( eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId) @@ -189,7 +189,8 @@ export async function getTraefikConfig( siteId: row.siteId, type: row.siteType, subnet: row.subnet, - exitNodeId: row.exitNodeId + exitNodeId: row.exitNodeId, + online: row.siteOnline } }); }); @@ -317,48 +318,67 @@ export async function getTraefikConfig( config_output.http.services![serviceName] = { loadBalancer: { - servers: (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - if ( - !target.ip || - !target.port || - !target.method - ) { + servers: (() => { + // Check if any sites are online + // THIS IS SO THAT THERE IS SOME IMMEDIATE FEEDBACK + // EVEN IF THE SITES HAVE NOT UPDATED YET FROM THE + // RECEIVE BANDWIDTH ENDPOINT. + + // TODO: HOW TO HANDLE ^^^^^^ BETTER + const anySitesOnline = ( + targets as TargetWithSite[] + ).some((target: TargetWithSite) => target.site.online); + + return (targets as TargetWithSite[]) + .filter((target: TargetWithSite) => { + if (!target.enabled) { return false; } - } else if (target.site.type === "newt") { - if ( - !target.internalPort || - !target.method || - !target.site.subnet - ) { + + // If any sites are online, exclude offline sites + if (anySitesOnline && !target.site.online) { return false; } - } - return true; - }) - .map((target: TargetWithSite) => { - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - return { - url: `${target.method}://${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { - const ip = target.site.subnet!.split("/")[0]; - return { - url: `${target.method}://${ip}:${target.internalPort}` - }; - } - }), + + if ( + target.site.type === "local" || + target.site.type === "wireguard" + ) { + if ( + !target.ip || + !target.port || + !target.method + ) { + return false; + } + } else if (target.site.type === "newt") { + if ( + !target.internalPort || + !target.method || + !target.site.subnet + ) { + return false; + } + } + return true; + }) + .map((target: TargetWithSite) => { + if ( + target.site.type === "local" || + target.site.type === "wireguard" + ) { + return { + url: `${target.method}://${target.ip}:${target.port}` + }; + } else if (target.site.type === "newt") { + const ip = + target.site.subnet!.split("/")[0]; + return { + url: `${target.method}://${ip}:${target.internalPort}` + }; + } + }); + })(), ...(resource.stickySession ? { sticky: { @@ -437,43 +457,57 @@ export async function getTraefikConfig( config_output[protocol].services[serviceName] = { loadBalancer: { - servers: (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - if (!target.ip || !target.port) { + servers: (() => { + // Check if any sites are online + const anySitesOnline = ( + targets as TargetWithSite[] + ).some((target: TargetWithSite) => target.site.online); + + return (targets as TargetWithSite[]) + .filter((target: TargetWithSite) => { + if (!target.enabled) { return false; } - } else if (target.site.type === "newt") { + + // If any sites are online, exclude offline sites + if (anySitesOnline && !target.site.online) { + return false; + } + if ( - !target.internalPort || - !target.site.subnet + target.site.type === "local" || + target.site.type === "wireguard" ) { - return false; + if (!target.ip || !target.port) { + return false; + } + } else if (target.site.type === "newt") { + if ( + !target.internalPort || + !target.site.subnet + ) { + return false; + } } - } - return true; - }) - .map((target: TargetWithSite) => { - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - return { - address: `${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { - const ip = target.site.subnet!.split("/")[0]; - return { - address: `${ip}:${target.internalPort}` - }; - } - }), + return true; + }) + .map((target: TargetWithSite) => { + if ( + target.site.type === "local" || + target.site.type === "wireguard" + ) { + return { + address: `${target.ip}:${target.port}` + }; + } else if (target.site.type === "newt") { + const ip = + target.site.subnet!.split("/")[0]; + return { + address: `${ip}:${target.internalPort}` + }; + } + }); + })(), ...(resource.stickySession ? { sticky: { From 7bbe1b2dbeb74d51ac60fc2c632e081d584cf2fc Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 19 Aug 2025 22:18:19 -0700 Subject: [PATCH 185/219] Align correctly --- src/app/[orgId]/settings/resources/ResourcesTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index 07fec698..c7bed441 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -666,7 +666,7 @@ export default function SitesTable({ onValueChange={handleTabChange} > -
+
{getSearchInput()}
{env.flags.enableClients && ( @@ -679,7 +679,7 @@ export default function SitesTable({ )}
-
+
{getActionButton()}
From 2907f222006e81da025b290729203679642a5bf2 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 19 Aug 2025 22:20:11 -0700 Subject: [PATCH 186/219] Fix server component issue --- src/app/admin/managed/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/admin/managed/page.tsx b/src/app/admin/managed/page.tsx index cb25ba5d..3d96ce8f 100644 --- a/src/app/admin/managed/page.tsx +++ b/src/app/admin/managed/page.tsx @@ -1,5 +1,3 @@ -"use client"; - import { SettingsContainer, SettingsSection, From 907dab7d0542e5b45857e5692646a13559db2347 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 10:26:32 -0700 Subject: [PATCH 187/219] Move docker podman question and add hybird question Allow empty config Continue to adjust config for hybrid --- install/main.go | 231 ++++++++++++++++------------- server/auth/sessions/app.ts | 4 +- server/emails/index.ts | 5 + server/lib/config.ts | 6 +- server/lib/readConfigFile.ts | 61 +++++++- server/lib/telemetry.ts | 2 +- server/routers/auth/securityKey.ts | 6 +- server/setup/copyInConfig.ts | 2 +- server/setup/ensureSetupToken.ts | 6 + 9 files changed, 207 insertions(+), 116 deletions(-) diff --git a/install/main.go b/install/main.go index ca68a769..55eab5e3 100644 --- a/install/main.go +++ b/install/main.go @@ -52,6 +52,7 @@ type Config struct { TraefikBouncerKey string DoCrowdsecInstall bool Secret string + HybridMode bool } type SupportedContainer string @@ -70,9 +71,6 @@ func main() { fmt.Println("") fmt.Println("Please make sure you have the following prerequisites:") fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.") - fmt.Println("- Point your domain to the VPS IP with A records.") - fmt.Println("") - fmt.Println("https://docs.digpangolin.com/self-host/dns-and-networking") fmt.Println("") fmt.Println("Lets get started!") fmt.Println("") @@ -89,71 +87,8 @@ func main() { } reader := bufio.NewReader(os.Stdin) - inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") - - chosenContainer := Docker - if strings.EqualFold(inputContainer, "docker") { - chosenContainer = Docker - } else if strings.EqualFold(inputContainer, "podman") { - chosenContainer = Podman - } else { - fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer) - os.Exit(1) - } - - if chosenContainer == Podman { - if !isPodmanInstalled() { - fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.") - os.Exit(1) - } - - if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil { - fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.") - fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.") - approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true) - if approved { - if os.Geteuid() != 0 { - fmt.Println("You need to run the installer as root for such a configuration.") - os.Exit(1) - } - - // Podman containers are not able to listen on privileged ports. The official recommendation is to - // container low-range ports as unprivileged ports. - // Linux only. - - if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil { - fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err) - os.Exit(1) - } - } else { - fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.") - } - } else { - fmt.Println("Unprivileged ports have been configured.") - } - - } else if chosenContainer == Docker { - // check if docker is not installed and the user is root - if !isDockerInstalled() { - if os.Geteuid() != 0 { - fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.") - os.Exit(1) - } - } - - // check if the user is in the docker group (linux only) - if !isUserInDockerGroup() { - fmt.Println("You are not in the docker group.") - fmt.Println("The installer will not be able to run docker commands without running it as root.") - os.Exit(1) - } - } else { - // This shouldn't happen unless there's a third container runtime. - os.Exit(1) - } var config Config - config.InstallationContainerType = chosenContainer // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { @@ -170,7 +105,9 @@ func main() { moveFile("config/docker-compose.yml", "docker-compose.yml") - if !isDockerInstalled() && runtime.GOOS == "linux" && chosenContainer == Docker { + config.InstallationContainerType = podmanOrDocker(reader) + + if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { if readBool(reader, "Docker is not installed. Would you like to install it?", true) { installDocker() // try to start docker service but ignore errors @@ -199,15 +136,15 @@ func main() { fmt.Println("\n=== Starting installation ===") - if (isDockerInstalled() && chosenContainer == Docker) || - (isPodmanInstalled() && chosenContainer == Podman) { + if (isDockerInstalled() && config.InstallationContainerType == Docker) || + (isPodmanInstalled() && config.InstallationContainerType == Podman) { if readBool(reader, "Would you like to install and start the containers?", true) { - if err := pullContainers(chosenContainer); err != nil { + if err := pullContainers(config.InstallationContainerType); err != nil { fmt.Println("Error: ", err) return } - if err := startContainers(chosenContainer); err != nil { + if err := startContainers(config.InstallationContainerType); err != nil { fmt.Println("Error: ", err) return } @@ -288,22 +225,89 @@ func main() { // Check if containers were started during this installation containersStarted := false - if (isDockerInstalled() && chosenContainer == Docker) || - (isPodmanInstalled() && chosenContainer == Podman) { + if (isDockerInstalled() && config.InstallationContainerType == Docker) || + (isPodmanInstalled() && config.InstallationContainerType == Podman) { // Try to fetch and display the token if containers are running containersStarted = true - printSetupToken(chosenContainer, config.DashboardDomain) + printSetupToken(config.InstallationContainerType, config.DashboardDomain) } // If containers weren't started or token wasn't found, show instructions if !containersStarted { - showSetupTokenInstructions(chosenContainer, config.DashboardDomain) + showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain) } fmt.Println("Installation complete!") fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } +func podmanOrDocker(reader *bufio.Reader) SupportedContainer { + inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") + + chosenContainer := Docker + if strings.EqualFold(inputContainer, "docker") { + chosenContainer = Docker + } else if strings.EqualFold(inputContainer, "podman") { + chosenContainer = Podman + } else { + fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer) + os.Exit(1) + } + + if chosenContainer == Podman { + if !isPodmanInstalled() { + fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.") + os.Exit(1) + } + + if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil { + fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.") + fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.") + approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true) + if approved { + if os.Geteuid() != 0 { + fmt.Println("You need to run the installer as root for such a configuration.") + os.Exit(1) + } + + // Podman containers are not able to listen on privileged ports. The official recommendation is to + // container low-range ports as unprivileged ports. + // Linux only. + + if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil { + fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err) + os.Exit(1) + } + } else { + fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.") + } + } else { + fmt.Println("Unprivileged ports have been configured.") + } + + } else if chosenContainer == Docker { + // check if docker is not installed and the user is root + if !isDockerInstalled() { + if os.Geteuid() != 0 { + fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.") + os.Exit(1) + } + } + + // check if the user is in the docker group (linux only) + if !isUserInDockerGroup() { + fmt.Println("You are not in the docker group.") + fmt.Println("The installer will not be able to run docker commands without running it as root.") + os.Exit(1) + } + } else { + // This shouldn't happen unless there's a third container runtime. + os.Exit(1) + } + + return chosenContainer +} + func readString(reader *bufio.Reader, prompt string, defaultValue string) string { if defaultValue != "" { fmt.Printf("%s (default: %s): ", prompt, defaultValue) @@ -318,6 +322,12 @@ func readString(reader *bufio.Reader, prompt string, defaultValue string) string return input } +func readStringNoDefault(reader *bufio.Reader, prompt string) string { + fmt.Print(prompt + ": ") + input, _ := reader.ReadString('\n') + return strings.TrimSpace(input) +} + func readPassword(prompt string, reader *bufio.Reader) string { if term.IsTerminal(int(syscall.Stdin)) { fmt.Print(prompt + ": ") @@ -347,6 +357,11 @@ func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool { return strings.ToLower(input) == "yes" } +func readBoolNoDefault(reader *bufio.Reader, prompt string) bool { + input := readStringNoDefault(reader, prompt+" (yes/no)") + return strings.ToLower(input) == "yes" +} + func readInt(reader *bufio.Reader, prompt string, defaultValue int) int { input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue)) if input == "" { @@ -362,42 +377,50 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") - config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") + config.HybridMode = readBoolNoDefault(reader, "Do you want to use hybrid mode?") - // Set default dashboard domain after base domain is collected - defaultDashboardDomain := "" - if config.BaseDomain != "" { - defaultDashboardDomain = "pangolin." + config.BaseDomain + if !config.HybridMode { + config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") + + // Set default dashboard domain after base domain is collected + defaultDashboardDomain := "" + if config.BaseDomain != "" { + defaultDashboardDomain = "pangolin." + config.BaseDomain + } + config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) + config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") } - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) - config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") + config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) - // Email configuration - fmt.Println("\n=== Email Configuration ===") - config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) + if !config.HybridMode { + // Email configuration + fmt.Println("\n=== Email Configuration ===") + config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) - if config.EnableEmail { - config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") - config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) - config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") - config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? - config.EmailNoReply = readString(reader, "Enter no-reply email address", "") - } + if config.EnableEmail { + config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") + config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) + config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") + config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? + config.EmailNoReply = readString(reader, "Enter no-reply email address", "") + } - // Validate required fields - if config.BaseDomain == "" { - fmt.Println("Error: Domain name is required") - os.Exit(1) - } - if config.DashboardDomain == "" { - fmt.Println("Error: Dashboard Domain name is required") - os.Exit(1) - } - if config.LetsEncryptEmail == "" { - fmt.Println("Error: Let's Encrypt email is required") - os.Exit(1) + + // Validate required fields + if config.BaseDomain == "" { + fmt.Println("Error: Domain name is required") + os.Exit(1) + } + if config.DashboardDomain == "" { + fmt.Println("Error: Dashboard Domain name is required") + os.Exit(1) + } + if config.LetsEncryptEmail == "" { + fmt.Println("Error: Let's Encrypt email is required") + os.Exit(1) + } } return config diff --git a/server/auth/sessions/app.ts b/server/auth/sessions/app.ts index 34d584f6..514bee00 100644 --- a/server/auth/sessions/app.ts +++ b/server/auth/sessions/app.ts @@ -24,8 +24,8 @@ export const SESSION_COOKIE_EXPIRES = 60 * 60 * config.getRawConfig().server.dashboard_session_length_hours; -export const COOKIE_DOMAIN = - "." + new URL(config.getRawConfig().app.dashboard_url).hostname; +export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url ? + "." + new URL(config.getRawConfig().app.dashboard_url!).hostname : undefined; export function generateSessionToken(): string { const bytes = new Uint8Array(20); diff --git a/server/emails/index.ts b/server/emails/index.ts index 42cfa39c..0388d3bb 100644 --- a/server/emails/index.ts +++ b/server/emails/index.ts @@ -6,6 +6,11 @@ import logger from "@server/logger"; import SMTPTransport from "nodemailer/lib/smtp-transport"; function createEmailClient() { + if (config.isHybridMode()) { + // LETS NOT WORRY ABOUT EMAILS IN HYBRID + return; + } + const emailConfig = config.getRawConfig().email; if (!emailConfig) { logger.warn( diff --git a/server/lib/config.ts b/server/lib/config.ts index 82932441..2437eaac 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -96,7 +96,11 @@ export class Config { if (!this.rawConfig) { throw new Error("Config not loaded. Call load() first."); } - license.setServerSecret(this.rawConfig.server.secret); + if (this.rawConfig.hybrid) { + // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID + return; + } + license.setServerSecret(this.rawConfig.server.secret!); await this.checkKeyStatus(); } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index fa05aebd..23098ac9 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -16,21 +16,28 @@ export const configSchema = z dashboard_url: z .string() .url() - .optional() .pipe(z.string().url()) - .transform((url) => url.toLowerCase()), + .transform((url) => url.toLowerCase()) + .optional(), log_level: z .enum(["debug", "info", "warn", "error"]) .optional() .default("info"), save_logs: z.boolean().optional().default(false), log_failed_attempts: z.boolean().optional().default(false), - telmetry: z + telemetry: z .object({ anonymous_usage: z.boolean().optional().default(true) }) .optional() .default({}) + }).optional().default({ + log_level: "info", + save_logs: false, + log_failed_attempts: false, + telemetry: { + anonymous_usage: true + } }), hybrid: z .object({ @@ -122,9 +129,25 @@ export const configSchema = z trust_proxy: z.number().int().gte(0).optional().default(1), secret: z .string() - .optional() .transform(getEnvOrYaml("SERVER_SECRET")) .pipe(z.string().min(8)) + .optional() + }).optional().default({ + integration_port: 3003, + external_port: 3000, + internal_port: 3001, + next_port: 3002, + internal_hostname: "pangolin", + session_cookie_name: "p_session_token", + resource_access_token_param: "p_token", + resource_access_token_headers: { + id: "P-Access-Token-Id", + token: "P-Access-Token" + }, + resource_session_request_param: "resource_session_request_param", + dashboard_session_length_hours: 720, + resource_session_length_hours: 720, + trust_proxy: 1 }), postgres: z .object({ @@ -282,6 +305,10 @@ export const configSchema = z if (data.flags?.disable_config_managed_domains) { return true; } + // If hybrid is defined, domains are not required + if (data.hybrid) { + return true; + } if (keys.length === 0) { return false; } @@ -290,6 +317,32 @@ export const configSchema = z { message: "At least one domain must be defined" } + ) + .refine( + (data) => { + // If hybrid is defined, server secret is not required + if (data.hybrid) { + return true; + } + // If hybrid is not defined, server secret must be defined + return data.server?.secret !== undefined && data.server.secret.length > 0; + }, + { + message: "Server secret must be defined" + } + ) + .refine( + (data) => { + // If hybrid is defined, dashboard_url is not required + if (data.hybrid) { + return true; + } + // If hybrid is not defined, dashboard_url must be defined + return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0; + }, + { + message: "Dashboard URL must be defined" + } ); export function readConfigFile() { diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts index ed3a8e73..cd000767 100644 --- a/server/lib/telemetry.ts +++ b/server/lib/telemetry.ts @@ -16,7 +16,7 @@ class TelemetryClient { private intervalId: NodeJS.Timeout | null = null; constructor() { - const enabled = config.getRawConfig().app.telmetry.anonymous_usage; + const enabled = config.getRawConfig().app.telemetry.anonymous_usage; this.enabled = enabled; const dev = process.env.ENVIRONMENT !== "prod"; diff --git a/server/routers/auth/securityKey.ts b/server/routers/auth/securityKey.ts index dad3c692..6b014986 100644 --- a/server/routers/auth/securityKey.ts +++ b/server/routers/auth/securityKey.ts @@ -36,16 +36,16 @@ import { verifyTotpCode } from "@server/auth/totp"; // The RP ID is the domain name of your application const rpID = (() => { - const url = new URL(config.getRawConfig().app.dashboard_url); + const url = config.getRawConfig().app.dashboard_url ? new URL(config.getRawConfig().app.dashboard_url!) : undefined; // For localhost, we must use 'localhost' without port - if (url.hostname === 'localhost') { + if (url?.hostname === 'localhost' || !url) { return 'localhost'; } return url.hostname; })(); const rpName = "Pangolin"; -const origin = config.getRawConfig().app.dashboard_url; +const origin = config.getRawConfig().app.dashboard_url || "localhost"; // Database-based challenge storage (replaces in-memory storage) // Challenges are stored in the webauthnChallenge table with automatic expiration diff --git a/server/setup/copyInConfig.ts b/server/setup/copyInConfig.ts index eccee475..b8c00192 100644 --- a/server/setup/copyInConfig.ts +++ b/server/setup/copyInConfig.ts @@ -8,7 +8,7 @@ export async function copyInConfig() { const endpoint = config.getRawConfig().gerbil.base_endpoint; const listenPort = config.getRawConfig().gerbil.start_port; - if (!config.getRawConfig().flags?.disable_config_managed_domains) { + if (!config.getRawConfig().flags?.disable_config_managed_domains && config.getRawConfig().domains) { await copyInDomains(); } diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 1734b5e6..49608218 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -3,6 +3,7 @@ import { eq } from "drizzle-orm"; import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; import moment from "moment"; import logger from "@server/logger"; +import config from "@server/lib/config"; const random: RandomReader = { read(bytes: Uint8Array): void { @@ -22,6 +23,11 @@ function generateId(length: number): string { } export async function ensureSetupToken() { + if (config.isHybridMode()) { + // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID + return; + } + try { // Check if a server admin already exists const [existingAdmin] = await db From 7de0761329633efb49148d5aa909d940162564f6 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 11:19:47 -0700 Subject: [PATCH 188/219] Rename function --- src/app/[orgId]/settings/resources/ResourcesTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index c7bed441..4ec49e9e 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -99,7 +99,7 @@ type ResourcesTableProps = { defaultView?: "proxy" | "internal"; }; -export default function SitesTable({ +export default function ResourcesTable({ resources, internalResources, orgId, From ad8ab63fd5476f62683f539a960fbfec25df9902 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 11:19:53 -0700 Subject: [PATCH 189/219] Reorging functions --- install/config/config.yml | 8 +- install/containers.go | 332 +++++++++++++++++++++++++++++++ install/input.go | 74 +++++++ install/main.go | 402 ++------------------------------------ 4 files changed, 425 insertions(+), 391 deletions(-) create mode 100644 install/containers.go create mode 100644 install/input.go diff --git a/install/config/config.yml b/install/config/config.yml index 2928b425..2fd9ee68 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,5 +1,5 @@ # To see all available options, please visit the docs: -# https://docs.digpangolin.com/self-host/dns-and-networking +# https://docs.digpangolin.com/self-host/advanced/config-file app: dashboard_url: "https://{{.DashboardDomain}}" @@ -36,3 +36,9 @@ flags: disable_signup_without_invite: true disable_user_create_org: false allow_raw_resources: true + +{{if and .HybridMode .HybridId .HybridSecret}} +hybrid: + id: "{{.HybridId}}" + secret: "{{.HybridSecret}}" +{{end}} \ No newline at end of file diff --git a/install/containers.go b/install/containers.go new file mode 100644 index 00000000..cea3a6ef --- /dev/null +++ b/install/containers.go @@ -0,0 +1,332 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "os/user" + "runtime" + "strconv" + "strings" + "time" +) + +func waitForContainer(containerName string, containerType SupportedContainer) error { + maxAttempts := 30 + retryInterval := time.Second * 2 + + for attempt := 0; attempt < maxAttempts; attempt++ { + // Check if container is running + cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName) + var out bytes.Buffer + cmd.Stdout = &out + + if err := cmd.Run(); err != nil { + // If the container doesn't exist or there's another error, wait and retry + time.Sleep(retryInterval) + continue + } + + isRunning := strings.TrimSpace(out.String()) == "true" + if isRunning { + return nil + } + + // Container exists but isn't running yet, wait and retry + time.Sleep(retryInterval) + } + + return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) +} + +func installDocker() error { + // Detect Linux distribution + cmd := exec.Command("cat", "/etc/os-release") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to detect Linux distribution: %v", err) + } + osRelease := string(output) + + // Detect system architecture + archCmd := exec.Command("uname", "-m") + archOutput, err := archCmd.Output() + if err != nil { + return fmt.Errorf("failed to detect system architecture: %v", err) + } + arch := strings.TrimSpace(string(archOutput)) + + // Map architecture to Docker's architecture naming + var dockerArch string + switch arch { + case "x86_64": + dockerArch = "amd64" + case "aarch64": + dockerArch = "arm64" + default: + return fmt.Errorf("unsupported architecture: %s", arch) + } + + var installCmd *exec.Cmd + switch { + case strings.Contains(osRelease, "ID=ubuntu"): + installCmd = exec.Command("bash", "-c", fmt.Sprintf(` + apt-get update && + apt-get install -y apt-transport-https ca-certificates curl software-properties-common && + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && + echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && + apt-get update && + apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + `, dockerArch)) + case strings.Contains(osRelease, "ID=debian"): + installCmd = exec.Command("bash", "-c", fmt.Sprintf(` + apt-get update && + apt-get install -y apt-transport-https ca-certificates curl software-properties-common && + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && + echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && + apt-get update && + apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + `, dockerArch)) + case strings.Contains(osRelease, "ID=fedora"): + // Detect Fedora version to handle DNF 5 changes + versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'") + versionOutput, err := versionCmd.Output() + var fedoraVersion int + if err == nil { + if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil { + fedoraVersion = v + } + } + + // Use appropriate DNF syntax based on version + var repoCmd string + if fedoraVersion >= 41 { + // DNF 5 syntax for Fedora 41+ + repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo" + } else { + // DNF 4 syntax for Fedora < 41 + repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo" + } + + installCmd = exec.Command("bash", "-c", fmt.Sprintf(` + dnf -y install dnf-plugins-core && + %s && + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + `, repoCmd)) + case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"): + installCmd = exec.Command("bash", "-c", ` + zypper install -y docker docker-compose && + systemctl enable docker + `) + case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"): + installCmd = exec.Command("bash", "-c", ` + dnf remove -y runc && + dnf -y install yum-utils && + dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo && + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin && + systemctl enable docker + `) + case strings.Contains(osRelease, "ID=amzn"): + installCmd = exec.Command("bash", "-c", ` + yum update -y && + yum install -y docker && + systemctl enable docker && + usermod -a -G docker ec2-user + `) + default: + return fmt.Errorf("unsupported Linux distribution") + } + + installCmd.Stdout = os.Stdout + installCmd.Stderr = os.Stderr + return installCmd.Run() +} + +func startDockerService() error { + if runtime.GOOS == "linux" { + cmd := exec.Command("systemctl", "enable", "--now", "docker") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } else if runtime.GOOS == "darwin" { + // On macOS, Docker is usually started via the Docker Desktop application + fmt.Println("Please start Docker Desktop manually on macOS.") + return nil + } + return fmt.Errorf("unsupported operating system for starting Docker service") +} + +func isDockerInstalled() bool { + return isContainerInstalled("docker") +} + +func isPodmanInstalled() bool { + return isContainerInstalled("podman") && isContainerInstalled("podman-compose") +} + +func isContainerInstalled(container string) bool { + cmd := exec.Command(container, "--version") + if err := cmd.Run(); err != nil { + return false + } + return true +} + +func isUserInDockerGroup() bool { + if runtime.GOOS == "darwin" { + // Docker group is not applicable on macOS + // So we assume that the user can run Docker commands + return true + } + + if os.Geteuid() == 0 { + return true // Root user can run Docker commands anyway + } + + // Check if the current user is in the docker group + if dockerGroup, err := user.LookupGroup("docker"); err == nil { + if currentUser, err := user.Current(); err == nil { + if currentUserGroupIds, err := currentUser.GroupIds(); err == nil { + for _, groupId := range currentUserGroupIds { + if groupId == dockerGroup.Gid { + return true + } + } + } + } + } + + // Eventually, if any of the checks fail, we assume the user cannot run Docker commands + return false +} + +// isDockerRunning checks if the Docker daemon is running by using the `docker info` command. +func isDockerRunning() bool { + cmd := exec.Command("docker", "info") + if err := cmd.Run(); err != nil { + return false + } + return true +} + +// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied +func executeDockerComposeCommandWithArgs(args ...string) error { + var cmd *exec.Cmd + var useNewStyle bool + + if !isDockerInstalled() { + return fmt.Errorf("docker is not installed") + } + + checkCmd := exec.Command("docker", "compose", "version") + if err := checkCmd.Run(); err == nil { + useNewStyle = true + } else { + checkCmd = exec.Command("docker-compose", "version") + if err := checkCmd.Run(); err == nil { + useNewStyle = false + } else { + return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available") + } + } + + if useNewStyle { + cmd = exec.Command("docker", append([]string{"compose"}, args...)...) + } else { + cmd = exec.Command("docker-compose", args...) + } + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// pullContainers pulls the containers using the appropriate command. +func pullContainers(containerType SupportedContainer) error { + fmt.Println("Pulling the container images...") + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) + } + + return nil + } + + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) +} + +// startContainers starts the containers using the appropriate command. +func startContainers(containerType SupportedContainer) error { + fmt.Println("Starting containers...") + + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { + return fmt.Errorf("failed start containers: %v", err) + } + + return nil + } + + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { + return fmt.Errorf("failed to start containers: %v", err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) +} + +// stopContainers stops the containers using the appropriate command. +func stopContainers(containerType SupportedContainer) error { + fmt.Println("Stopping containers...") + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil { + return fmt.Errorf("failed to stop containers: %v", err) + } + + return nil + } + + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil { + return fmt.Errorf("failed to stop containers: %v", err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) +} + +// restartContainer restarts a specific container using the appropriate command. +func restartContainer(container string, containerType SupportedContainer) error { + fmt.Println("Restarting containers...") + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil { + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + } + + return nil + } + + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) +} diff --git a/install/input.go b/install/input.go new file mode 100644 index 00000000..cf8fd7a3 --- /dev/null +++ b/install/input.go @@ -0,0 +1,74 @@ +package main + +import ( + "bufio" + "fmt" + "strings" + "syscall" + + "golang.org/x/term" +) + +func readString(reader *bufio.Reader, prompt string, defaultValue string) string { + if defaultValue != "" { + fmt.Printf("%s (default: %s): ", prompt, defaultValue) + } else { + fmt.Print(prompt + ": ") + } + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + if input == "" { + return defaultValue + } + return input +} + +func readStringNoDefault(reader *bufio.Reader, prompt string) string { + fmt.Print(prompt + ": ") + input, _ := reader.ReadString('\n') + return strings.TrimSpace(input) +} + +func readPassword(prompt string, reader *bufio.Reader) string { + if term.IsTerminal(int(syscall.Stdin)) { + fmt.Print(prompt + ": ") + // Read password without echo if we're in a terminal + password, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() // Add a newline since ReadPassword doesn't add one + if err != nil { + return "" + } + input := strings.TrimSpace(string(password)) + if input == "" { + return readPassword(prompt, reader) + } + return input + } else { + // Fallback to reading from stdin if not in a terminal + return readString(reader, prompt, "") + } +} + +func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool { + defaultStr := "no" + if defaultValue { + defaultStr = "yes" + } + input := readString(reader, prompt+" (yes/no)", defaultStr) + return strings.ToLower(input) == "yes" +} + +func readBoolNoDefault(reader *bufio.Reader, prompt string) bool { + input := readStringNoDefault(reader, prompt+" (yes/no)") + return strings.ToLower(input) == "yes" +} + +func readInt(reader *bufio.Reader, prompt string, defaultValue int) int { + input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue)) + if input == "" { + return defaultValue + } + value := defaultValue + fmt.Sscanf(input, "%d", &value) + return value +} diff --git a/install/main.go b/install/main.go index 55eab5e3..8b5284f7 100644 --- a/install/main.go +++ b/install/main.go @@ -10,17 +10,12 @@ import ( "math/rand" "os" "os/exec" - "os/user" "path/filepath" "runtime" - "strconv" "strings" - "syscall" "text/template" "time" "net" - - "golang.org/x/term" ) // DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD @@ -52,7 +47,9 @@ type Config struct { TraefikBouncerKey string DoCrowdsecInstall bool Secret string - HybridMode bool + HybridMode bool + HybridId string + HybridSecret string } type SupportedContainer string @@ -308,70 +305,6 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer { return chosenContainer } -func readString(reader *bufio.Reader, prompt string, defaultValue string) string { - if defaultValue != "" { - fmt.Printf("%s (default: %s): ", prompt, defaultValue) - } else { - fmt.Print(prompt + ": ") - } - input, _ := reader.ReadString('\n') - input = strings.TrimSpace(input) - if input == "" { - return defaultValue - } - return input -} - -func readStringNoDefault(reader *bufio.Reader, prompt string) string { - fmt.Print(prompt + ": ") - input, _ := reader.ReadString('\n') - return strings.TrimSpace(input) -} - -func readPassword(prompt string, reader *bufio.Reader) string { - if term.IsTerminal(int(syscall.Stdin)) { - fmt.Print(prompt + ": ") - // Read password without echo if we're in a terminal - password, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Println() // Add a newline since ReadPassword doesn't add one - if err != nil { - return "" - } - input := strings.TrimSpace(string(password)) - if input == "" { - return readPassword(prompt, reader) - } - return input - } else { - // Fallback to reading from stdin if not in a terminal - return readString(reader, prompt, "") - } -} - -func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool { - defaultStr := "no" - if defaultValue { - defaultStr = "yes" - } - input := readString(reader, prompt+" (yes/no)", defaultStr) - return strings.ToLower(input) == "yes" -} - -func readBoolNoDefault(reader *bufio.Reader, prompt string) bool { - input := readStringNoDefault(reader, prompt+" (yes/no)") - return strings.ToLower(input) == "yes" -} - -func readInt(reader *bufio.Reader, prompt string, defaultValue int) int { - input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue)) - if input == "" { - return defaultValue - } - value := defaultValue - fmt.Sscanf(input, "%d", &value) - return value -} - func collectUserInput(reader *bufio.Reader) Config { config := Config{} @@ -379,6 +312,15 @@ func collectUserInput(reader *bufio.Reader) Config { fmt.Println("\n=== Basic Configuration ===") config.HybridMode = readBoolNoDefault(reader, "Do you want to use hybrid mode?") + if config.HybridMode { + alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false) + + if alreadyHaveCreds { + config.HybridId = readString(reader, "Enter your hybrid ID", "") + config.HybridSecret = readString(reader, "Enter your hybrid secret", "") + } + } + if !config.HybridMode { config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") @@ -501,298 +443,6 @@ func createConfigFiles(config Config) error { return nil } - -func installDocker() error { - // Detect Linux distribution - cmd := exec.Command("cat", "/etc/os-release") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to detect Linux distribution: %v", err) - } - osRelease := string(output) - - // Detect system architecture - archCmd := exec.Command("uname", "-m") - archOutput, err := archCmd.Output() - if err != nil { - return fmt.Errorf("failed to detect system architecture: %v", err) - } - arch := strings.TrimSpace(string(archOutput)) - - // Map architecture to Docker's architecture naming - var dockerArch string - switch arch { - case "x86_64": - dockerArch = "amd64" - case "aarch64": - dockerArch = "arm64" - default: - return fmt.Errorf("unsupported architecture: %s", arch) - } - - var installCmd *exec.Cmd - switch { - case strings.Contains(osRelease, "ID=ubuntu"): - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` - apt-get update && - apt-get install -y apt-transport-https ca-certificates curl software-properties-common && - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && - echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && - apt-get update && - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - `, dockerArch)) - case strings.Contains(osRelease, "ID=debian"): - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` - apt-get update && - apt-get install -y apt-transport-https ca-certificates curl software-properties-common && - curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && - echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && - apt-get update && - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - `, dockerArch)) - case strings.Contains(osRelease, "ID=fedora"): - // Detect Fedora version to handle DNF 5 changes - versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'") - versionOutput, err := versionCmd.Output() - var fedoraVersion int - if err == nil { - if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil { - fedoraVersion = v - } - } - - // Use appropriate DNF syntax based on version - var repoCmd string - if fedoraVersion >= 41 { - // DNF 5 syntax for Fedora 41+ - repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo" - } else { - // DNF 4 syntax for Fedora < 41 - repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo" - } - - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` - dnf -y install dnf-plugins-core && - %s && - dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - `, repoCmd)) - case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"): - installCmd = exec.Command("bash", "-c", ` - zypper install -y docker docker-compose && - systemctl enable docker - `) - case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"): - installCmd = exec.Command("bash", "-c", ` - dnf remove -y runc && - dnf -y install yum-utils && - dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo && - dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin && - systemctl enable docker - `) - case strings.Contains(osRelease, "ID=amzn"): - installCmd = exec.Command("bash", "-c", ` - yum update -y && - yum install -y docker && - systemctl enable docker && - usermod -a -G docker ec2-user - `) - default: - return fmt.Errorf("unsupported Linux distribution") - } - - installCmd.Stdout = os.Stdout - installCmd.Stderr = os.Stderr - return installCmd.Run() -} - -func startDockerService() error { - if runtime.GOOS == "linux" { - cmd := exec.Command("systemctl", "enable", "--now", "docker") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - } else if runtime.GOOS == "darwin" { - // On macOS, Docker is usually started via the Docker Desktop application - fmt.Println("Please start Docker Desktop manually on macOS.") - return nil - } - return fmt.Errorf("unsupported operating system for starting Docker service") -} - -func isDockerInstalled() bool { - return isContainerInstalled("docker") -} - -func isPodmanInstalled() bool { - return isContainerInstalled("podman") && isContainerInstalled("podman-compose") -} - -func isContainerInstalled(container string) bool { - cmd := exec.Command(container, "--version") - if err := cmd.Run(); err != nil { - return false - } - return true -} - -func isUserInDockerGroup() bool { - if runtime.GOOS == "darwin" { - // Docker group is not applicable on macOS - // So we assume that the user can run Docker commands - return true - } - - if os.Geteuid() == 0 { - return true // Root user can run Docker commands anyway - } - - // Check if the current user is in the docker group - if dockerGroup, err := user.LookupGroup("docker"); err == nil { - if currentUser, err := user.Current(); err == nil { - if currentUserGroupIds, err := currentUser.GroupIds(); err == nil { - for _, groupId := range currentUserGroupIds { - if groupId == dockerGroup.Gid { - return true - } - } - } - } - } - - // Eventually, if any of the checks fail, we assume the user cannot run Docker commands - return false -} - -// isDockerRunning checks if the Docker daemon is running by using the `docker info` command. -func isDockerRunning() bool { - cmd := exec.Command("docker", "info") - if err := cmd.Run(); err != nil { - return false - } - return true -} - -// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied -func executeDockerComposeCommandWithArgs(args ...string) error { - var cmd *exec.Cmd - var useNewStyle bool - - if !isDockerInstalled() { - return fmt.Errorf("docker is not installed") - } - - checkCmd := exec.Command("docker", "compose", "version") - if err := checkCmd.Run(); err == nil { - useNewStyle = true - } else { - checkCmd = exec.Command("docker-compose", "version") - if err := checkCmd.Run(); err == nil { - useNewStyle = false - } else { - return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available") - } - } - - if useNewStyle { - cmd = exec.Command("docker", append([]string{"compose"}, args...)...) - } else { - cmd = exec.Command("docker-compose", args...) - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -// pullContainers pulls the containers using the appropriate command. -func pullContainers(containerType SupportedContainer) error { - fmt.Println("Pulling the container images...") - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil { - return fmt.Errorf("failed to pull the containers: %v", err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { - return fmt.Errorf("failed to pull the containers: %v", err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} - -// startContainers starts the containers using the appropriate command. -func startContainers(containerType SupportedContainer) error { - fmt.Println("Starting containers...") - - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { - return fmt.Errorf("failed start containers: %v", err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { - return fmt.Errorf("failed to start containers: %v", err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} - -// stopContainers stops the containers using the appropriate command. -func stopContainers(containerType SupportedContainer) error { - fmt.Println("Stopping containers...") - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil { - return fmt.Errorf("failed to stop containers: %v", err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil { - return fmt.Errorf("failed to stop containers: %v", err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} - -// restartContainer restarts a specific container using the appropriate command. -func restartContainer(container string, containerType SupportedContainer) error { - fmt.Println("Restarting containers...") - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil { - return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { - return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} - func copyFile(src, dst string) error { source, err := os.Open(src) if err != nil { @@ -818,34 +468,6 @@ func moveFile(src, dst string) error { return os.Remove(src) } -func waitForContainer(containerName string, containerType SupportedContainer) error { - maxAttempts := 30 - retryInterval := time.Second * 2 - - for attempt := 0; attempt < maxAttempts; attempt++ { - // Check if container is running - cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName) - var out bytes.Buffer - cmd.Stdout = &out - - if err := cmd.Run(); err != nil { - // If the container doesn't exist or there's another error, wait and retry - time.Sleep(retryInterval) - continue - } - - isRunning := strings.TrimSpace(out.String()) == "true" - if isRunning { - return nil - } - - // Container exists but isn't running yet, wait and retry - time.Sleep(retryInterval) - } - - return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) -} - func printSetupToken(containerType SupportedContainer, dashboardDomain string) { fmt.Println("Waiting for Pangolin to generate setup token...") From 8273554a1cb32a5597efb9d715186e38dfff2207 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 12:40:21 -0700 Subject: [PATCH 190/219] Hybrid install mode done? --- install/config/config.yml | 20 +-- install/config/docker-compose.yml | 12 +- install/config/traefik/traefik_config.yml | 18 +- install/main.go | 190 ++++++++++++---------- 4 files changed, 140 insertions(+), 100 deletions(-) diff --git a/install/config/config.yml b/install/config/config.yml index 2fd9ee68..0dde388a 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,6 +1,15 @@ # To see all available options, please visit the docs: # https://docs.digpangolin.com/self-host/advanced/config-file +gerbil: + start_port: 51820 + base_endpoint: "{{.DashboardDomain}}" +{{if .HybridMode}} +hybrid: + id: "{{.HybridId}}" + secret: "{{.HybridSecret}}" + +{{else}} app: dashboard_url: "https://{{.DashboardDomain}}" log_level: "info" @@ -17,11 +26,6 @@ server: methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] allowed_headers: ["X-CSRF-Token", "Content-Type"] credentials: false - -gerbil: - start_port: 51820 - base_endpoint: "{{.DashboardDomain}}" - {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" @@ -30,15 +34,9 @@ email: smtp_pass: "{{.EmailSMTPPass}}" no_reply: "{{.EmailNoReply}}" {{end}} - flags: require_email_verification: {{.EnableEmail}} disable_signup_without_invite: true disable_user_create_org: false allow_raw_resources: true - -{{if and .HybridMode .HybridId .HybridSecret}} -hybrid: - id: "{{.HybridId}}" - secret: "{{.HybridSecret}}" {{end}} \ No newline at end of file diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 44af4199..97b30317 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -6,6 +6,8 @@ services: restart: unless-stopped volumes: - ./config:/app/config + - pangolin-data:/var/certificates + - pangolin-data:/var/dynamic healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"] interval: "10s" @@ -31,8 +33,8 @@ services: ports: - 51820:51820/udp - 21820:21820/udp - - 443:443 # Port for traefik because of the network_mode - - 80:80 # Port for traefik because of the network_mode + - 443:{{if .HybridMode}}8443{{else}}443{{end}} + - 80:80 {{end}} traefik: image: docker.io/traefik:v3.5 @@ -54,9 +56,15 @@ services: - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs + # Shared volume for certificates and dynamic config in file mode + - pangolin-data:/var/certificates:ro + - pangolin-data:/var/dynamic:ro networks: default: driver: bridge name: pangolin {{if .EnableIPv6}} enable_ipv6: true{{end}} + +volumes: + pangolin-data: diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml index 842786fa..dd0ba1b2 100644 --- a/install/config/traefik/traefik_config.yml +++ b/install/config/traefik/traefik_config.yml @@ -3,12 +3,17 @@ api: dashboard: true providers: +{{if not .HybridMode}} http: endpoint: "http://pangolin:3001/api/v1/traefik-config" pollInterval: "5s" file: filename: "/etc/traefik/dynamic_config.yml" - +{{else}} + file: + directory: "/var/dynamic" + watch: true +{{end}} experimental: plugins: badger: @@ -22,7 +27,7 @@ log: maxBackups: 3 maxAge: 3 compress: true - +{{if not .HybridMode}} certificatesResolvers: letsencrypt: acme: @@ -31,7 +36,7 @@ certificatesResolvers: email: "{{.LetsEncryptEmail}}" storage: "/letsencrypt/acme.json" caServer: "https://acme-v02.api.letsencrypt.org/directory" - +{{end}} entryPoints: web: address: ":80" @@ -40,9 +45,12 @@ entryPoints: transport: respondingTimeouts: readTimeout: "30m" - http: +{{if not .HybridMode}} http: tls: - certResolver: "letsencrypt" + certResolver: "letsencrypt"{{end}} serversTransport: insecureSkipVerify: true + +ping: + entryPoint: "web" \ No newline at end of file diff --git a/install/main.go b/install/main.go index 8b5284f7..1dd0b37c 100644 --- a/install/main.go +++ b/install/main.go @@ -65,12 +65,9 @@ func main() { fmt.Println("Welcome to the Pangolin installer!") fmt.Println("This installer will help you set up Pangolin on your server.") - fmt.Println("") - fmt.Println("Please make sure you have the following prerequisites:") + fmt.Println("\nPlease make sure you have the following prerequisites:") fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.") - fmt.Println("") - fmt.Println("Lets get started!") - fmt.Println("") + fmt.Println("\nLets get started!") if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS for _, p := range []int{80, 443} { @@ -102,51 +99,52 @@ func main() { moveFile("config/docker-compose.yml", "docker-compose.yml") - config.InstallationContainerType = podmanOrDocker(reader) - - if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { - if readBool(reader, "Docker is not installed. Would you like to install it?", true) { - installDocker() - // try to start docker service but ignore errors - if err := startDockerService(); err != nil { - fmt.Println("Error starting Docker service:", err) - } else { - fmt.Println("Docker service started successfully!") - } - // wait 10 seconds for docker to start checking if docker is running every 2 seconds - fmt.Println("Waiting for Docker to start...") - for i := 0; i < 5; i++ { - if isDockerRunning() { - fmt.Println("Docker is running!") - break - } - fmt.Println("Docker is not running yet, waiting...") - time.Sleep(2 * time.Second) - } - if !isDockerRunning() { - fmt.Println("Docker is still not running after 10 seconds. Please check the installation.") - os.Exit(1) - } - fmt.Println("Docker installed successfully!") - } - } + fmt.Println("\nConfiguration files created successfully!") fmt.Println("\n=== Starting installation ===") - if (isDockerInstalled() && config.InstallationContainerType == Docker) || - (isPodmanInstalled() && config.InstallationContainerType == Podman) { - if readBool(reader, "Would you like to install and start the containers?", true) { - if err := pullContainers(config.InstallationContainerType); err != nil { - fmt.Println("Error: ", err) - return - } + if readBool(reader, "Would you like to install and start the containers?", true) { - if err := startContainers(config.InstallationContainerType); err != nil { - fmt.Println("Error: ", err) - return + config.InstallationContainerType = podmanOrDocker(reader) + + if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { + if readBool(reader, "Docker is not installed. Would you like to install it?", true) { + installDocker() + // try to start docker service but ignore errors + if err := startDockerService(); err != nil { + fmt.Println("Error starting Docker service:", err) + } else { + fmt.Println("Docker service started successfully!") + } + // wait 10 seconds for docker to start checking if docker is running every 2 seconds + fmt.Println("Waiting for Docker to start...") + for i := 0; i < 5; i++ { + if isDockerRunning() { + fmt.Println("Docker is running!") + break + } + fmt.Println("Docker is not running yet, waiting...") + time.Sleep(2 * time.Second) + } + if !isDockerRunning() { + fmt.Println("Docker is still not running after 10 seconds. Please check the installation.") + os.Exit(1) + } + fmt.Println("Docker installed successfully!") } } + + if err := pullContainers(config.InstallationContainerType); err != nil { + fmt.Println("Error: ", err) + return + } + + if err := startContainers(config.InstallationContainerType); err != nil { + fmt.Println("Error: ", err) + return + } } + } else { fmt.Println("Looks like you already installed, so I am going to do the setup...") @@ -171,15 +169,16 @@ func main() { config = collectUserInput(reader) } } - } - // Check if Pangolin is already installed with hybrid section - if checkIsPangolinInstalledWithHybrid() { - fmt.Println("\n=== Convert to Self-Host Node ===") - if readBool(reader, "Do you want to convert this Pangolin instance into a manage self-host node?", true) { - fmt.Println("hello world") - return - } + // Check if Pangolin is already installed with hybrid section + // if checkIsPangolinInstalledWithHybrid() { + // fmt.Println("\n=== Convert to Self-Host Node ===") + // if readBool(reader, "Do you want to convert this Pangolin instance into a managed self-host node?", true) { + // fmt.Println("hello world") + // return + // } + // } + } if !checkIsCrowdsecInstalledInCompose() { @@ -217,25 +216,30 @@ func main() { } } - // Setup Token Section - fmt.Println("\n=== Setup Token ===") + if !config.HybridMode { + // Setup Token Section + fmt.Println("\n=== Setup Token ===") - // Check if containers were started during this installation - containersStarted := false - if (isDockerInstalled() && config.InstallationContainerType == Docker) || - (isPodmanInstalled() && config.InstallationContainerType == Podman) { - // Try to fetch and display the token if containers are running - containersStarted = true - printSetupToken(config.InstallationContainerType, config.DashboardDomain) + // Check if containers were started during this installation + containersStarted := false + if (isDockerInstalled() && config.InstallationContainerType == Docker) || + (isPodmanInstalled() && config.InstallationContainerType == Podman) { + // Try to fetch and display the token if containers are running + containersStarted = true + printSetupToken(config.InstallationContainerType, config.DashboardDomain) + } + + // If containers weren't started or token wasn't found, show instructions + if !containersStarted { + showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain) + } } - // If containers weren't started or token wasn't found, show instructions - if !containersStarted { - showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain) - } + fmt.Println("\nInstallation complete!") - fmt.Println("Installation complete!") - fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) + if !config.HybridMode { + fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) + } } func podmanOrDocker(reader *bufio.Reader) SupportedContainer { @@ -310,18 +314,32 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") - config.HybridMode = readBoolNoDefault(reader, "Do you want to use hybrid mode?") + for { + response := readString(reader, "Do you want to install Pangolin as a cloud-managed self-hosted node? (yes/no)", "") + if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") { + config.HybridMode = true + break + } else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") { + config.HybridMode = false + break + } + fmt.Println("Please answer 'yes' or 'no'") + } if config.HybridMode { alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false) - + if alreadyHaveCreds { config.HybridId = readString(reader, "Enter your hybrid ID", "") config.HybridSecret = readString(reader, "Enter your hybrid secret", "") + } else { + // Just print instructions for right now + fmt.Println("Please visit https://pangolin.fossorial.io, create a self hosted node, and return with the credentials.") } - } - if !config.HybridMode { + config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "") + config.InstallGerbil = true + } else { config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") // Set default dashboard domain after base domain is collected @@ -331,16 +349,12 @@ func collectUserInput(reader *bufio.Reader) Config { } config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") - } + config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) - config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) - config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) - - if !config.HybridMode { // Email configuration fmt.Println("\n=== Email Configuration ===") config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) - + if config.EnableEmail { config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) @@ -348,23 +362,29 @@ func collectUserInput(reader *bufio.Reader) Config { config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } - - + // Validate required fields if config.BaseDomain == "" { fmt.Println("Error: Domain name is required") os.Exit(1) } - if config.DashboardDomain == "" { - fmt.Println("Error: Dashboard Domain name is required") - os.Exit(1) - } if config.LetsEncryptEmail == "" { fmt.Println("Error: Let's Encrypt email is required") os.Exit(1) } } + // Advanced configuration + + fmt.Println("\n=== Advanced Configuration ===") + + config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) + + if config.DashboardDomain == "" { + fmt.Println("Error: Dashboard Domain name is required") + os.Exit(1) + } + return config } @@ -393,6 +413,11 @@ func createConfigFiles(config Config) error { return nil } + // the hybrid does not need the dynamic config + if config.HybridMode && strings.Contains(path, "dynamic_config.yml") { + return nil + } + // skip .DS_Store if strings.Contains(path, ".DS_Store") { return nil @@ -443,6 +468,7 @@ func createConfigFiles(config Config) error { return nil } + func copyFile(src, dst string) error { source, err := os.Open(src) if err != nil { From 2c273a85d8410acf1c7da4a625c7279aa03f5360 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 20 Aug 2025 14:56:23 -0700 Subject: [PATCH 191/219] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 48 ++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index ecbc65ca..c91c7ed8 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -13,11 +13,11 @@ "welcome": "Добре дошли!", "welcomeTo": "Добре дошли в", "componentsCreateOrg": "Създай организация", - "componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.", - "componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.", - "dismiss": "Dismiss", - "componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.", - "componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!", + "componentsMember": "Вие сте част от {count, plural, =0 {нула организации} one {една организация} other {# организации}}.", + "componentsInvalidKey": "Засечен е невалиден или изтекъл лиценз. Проверете лицензионните условия, за да се възползвате от всички функционалности.", + "dismiss": "Отхвърляне", + "componentsLicenseViolation": "Нарушение на лиценза: Сървърът използва {usedSites} сайта, което надвишава лицензионния лимит от {maxSites} сайта. Проверете лицензионните условия, за да се възползвате от всички функционалности.", + "componentsSupporterMessage": "Благодарим ви, че подкрепяте Pangolin като {tier}!", "inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.", "inviteErrorUser": "We're sorry, but it looks like the invite you're trying to access is not for this user.", "inviteLoginUser": "Please make sure you're logged in as the correct user.", @@ -29,24 +29,24 @@ "inviteNotAccepted": "Invite Not Accepted", "authCreateAccount": "Create an account to get started", "authNoAccount": "Don't have an account?", - "email": "Email", - "password": "Password", - "confirmPassword": "Confirm Password", - "createAccount": "Create Account", - "viewSettings": "View settings", - "delete": "Delete", - "name": "Name", - "online": "Online", - "offline": "Offline", - "site": "Site", - "dataIn": "Data In", - "dataOut": "Data Out", - "connectionType": "Connection Type", - "tunnelType": "Tunnel Type", - "local": "Local", - "edit": "Edit", - "siteConfirmDelete": "Confirm Delete Site", - "siteDelete": "Delete Site", + "email": "Имейл", + "password": "Парола", + "confirmPassword": "Потвърждение на паролата", + "createAccount": "Създаване на профил", + "viewSettings": "Преглед на настройките", + "delete": "Изтриване", + "name": "Име", + "online": "На линия", + "offline": "Извън линия", + "site": "Сайт", + "dataIn": "Входящ трафик", + "dataOut": "Изходящ трафик", + "connectionType": "Вид на връзката", + "tunnelType": "Вид на тунела", + "local": "Локална", + "edit": "Редактиране", + "siteConfirmDelete": "Потвърждение на изтриване на сайта", + "siteDelete": "Изтриване на сайта", "siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.", "siteMessageConfirm": "To confirm, please type the name of the site below.", "siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?", @@ -85,7 +85,7 @@ "siteErrorDelete": "Error deleting site", "siteErrorUpdate": "Failed to update site", "siteErrorUpdateDescription": "An error occurred while updating the site.", - "siteUpdated": "Site updated", + "siteUpdated": "Сайтът е обновен", "siteUpdatedDescription": "The site has been updated.", "siteGeneralDescription": "Configure the general settings for this site", "siteSettingDescription": "Configure the settings on your site", From 49f0f6ec7d9923e8a8d8d8b678ad1bbdae09f881 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 17:00:52 -0700 Subject: [PATCH 192/219] Installer working with hybrid --- install/config.go | 70 +++++++++++++-------------- install/main.go | 84 ++++++++++++++------------------ install/quickStart.go | 109 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 85 deletions(-) create mode 100644 install/quickStart.go diff --git a/install/config.go b/install/config.go index 3be62601..e75dd50d 100644 --- a/install/config.go +++ b/install/config.go @@ -37,15 +37,28 @@ type DynamicConfig struct { } `yaml:"http"` } -// ConfigValues holds the extracted configuration values -type ConfigValues struct { +// TraefikConfigValues holds the extracted configuration values +type TraefikConfigValues struct { DashboardDomain string LetsEncryptEmail string BadgerVersion string } +// AppConfig represents the app section of the config.yml +type AppConfig struct { + App struct { + DashboardURL string `yaml:"dashboard_url"` + LogLevel string `yaml:"log_level"` + } `yaml:"app"` +} + +type AppConfigValues struct { + DashboardURL string + LogLevel string +} + // ReadTraefikConfig reads and extracts values from Traefik configuration files -func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) { +func ReadTraefikConfig(mainConfigPath string) (*TraefikConfigValues, error) { // Read main config file mainConfigData, err := os.ReadFile(mainConfigPath) if err != nil { @@ -57,48 +70,33 @@ func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, return nil, fmt.Errorf("error parsing main config file: %w", err) } - // Read dynamic config file - dynamicConfigData, err := os.ReadFile(dynamicConfigPath) - if err != nil { - return nil, fmt.Errorf("error reading dynamic config file: %w", err) - } - - var dynamicConfig DynamicConfig - if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil { - return nil, fmt.Errorf("error parsing dynamic config file: %w", err) - } - // Extract values - values := &ConfigValues{ + values := &TraefikConfigValues{ BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version, LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email, } - // Extract DashboardDomain from router rules - // Look for it in the main router rules - for _, router := range dynamicConfig.HTTP.Routers { - if router.Rule != "" { - // Extract domain from Host(`mydomain.com`) - if domain := extractDomainFromRule(router.Rule); domain != "" { - values.DashboardDomain = domain - break - } - } - } - return values, nil } -// extractDomainFromRule extracts the domain from a router rule -func extractDomainFromRule(rule string) string { - // Look for the Host(`mydomain.com`) pattern - if start := findPattern(rule, "Host(`"); start != -1 { - end := findPattern(rule[start:], "`)") - if end != -1 { - return rule[start+6 : start+end] - } +func ReadAppConfig(configPath string) (*AppConfigValues, error) { + // Read config file + configData, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("error reading config file: %w", err) } - return "" + + var appConfig AppConfig + if err := yaml.Unmarshal(configData, &appConfig); err != nil { + return nil, fmt.Errorf("error parsing config file: %w", err) + } + + values := &AppConfigValues{ + DashboardURL: appConfig.App.DashboardURL, + LogLevel: appConfig.App.LogLevel, + } + + return values, nil } // findPattern finds the start of a pattern in a string diff --git a/install/main.go b/install/main.go index 1dd0b37c..3e9c093c 100644 --- a/install/main.go +++ b/install/main.go @@ -92,6 +92,26 @@ func main() { config.DoCrowdsecInstall = false config.Secret = generateRandomSecretKey() + fmt.Println("\n=== Generating Configuration Files ===") + + // If the secret and id are not generated then generate them + if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") { + // fmt.Println("Requesting hybrid credentials from cloud...") + credentials, err := requestHybridCredentials() + if err != nil { + fmt.Printf("Error requesting hybrid credentials: %v\n", err) + fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.") + os.Exit(1) + } + config.HybridId = credentials.RemoteExitNodeId + config.HybridSecret = credentials.Secret + fmt.Printf("Your managed credentials have been obtained successfully.\n") + fmt.Printf(" ID: %s\n", config.HybridId) + fmt.Printf(" Secret: %s\n", config.HybridSecret) + fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.") + readBool(reader, "Have you adopted your node?", true) + } + if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) @@ -146,42 +166,10 @@ func main() { } } else { - fmt.Println("Looks like you already installed, so I am going to do the setup...") - - // Read existing config to get DashboardDomain - traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") - if err != nil { - fmt.Printf("Warning: Could not read existing config: %v\n", err) - fmt.Println("You may need to manually enter your domain information.") - config = collectUserInput(reader) - } else { - config.DashboardDomain = traefikConfig.DashboardDomain - config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail - config.BadgerVersion = traefikConfig.BadgerVersion - - // Show detected values and allow user to confirm or re-enter - fmt.Println("Detected existing configuration:") - fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain) - fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail) - fmt.Printf("Badger Version: %s\n", config.BadgerVersion) - - if !readBool(reader, "Are these values correct?", true) { - config = collectUserInput(reader) - } - } - - // Check if Pangolin is already installed with hybrid section - // if checkIsPangolinInstalledWithHybrid() { - // fmt.Println("\n=== Convert to Self-Host Node ===") - // if readBool(reader, "Do you want to convert this Pangolin instance into a managed self-host node?", true) { - // fmt.Println("hello world") - // return - // } - // } - + fmt.Println("Looks like you already installed Pangolin!") } - if !checkIsCrowdsecInstalledInCompose() { + if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed if readBool(reader, "Would you like to install CrowdSec?", false) { @@ -190,12 +178,18 @@ func main() { // BUG: crowdsec installation will be skipped if the user chooses to install on the first installation. if readBool(reader, "Are you willing to manage CrowdSec?", false) { if config.DashboardDomain == "" { - traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") + traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml") if err != nil { fmt.Printf("Error reading config: %v\n", err) return } - config.DashboardDomain = traefikConfig.DashboardDomain + appConfig, err := ReadAppConfig("config/config.yml") + if err != nil { + fmt.Printf("Error reading config: %v\n", err) + return + } + + config.DashboardDomain = appConfig.DashboardURL config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail config.BadgerVersion = traefikConfig.BadgerVersion @@ -237,7 +231,7 @@ func main() { fmt.Println("\nInstallation complete!") - if !config.HybridMode { + if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() { fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } } @@ -327,15 +321,12 @@ func collectUserInput(reader *bufio.Reader) Config { } if config.HybridMode { - alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false) + alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) if alreadyHaveCreds { - config.HybridId = readString(reader, "Enter your hybrid ID", "") - config.HybridSecret = readString(reader, "Enter your hybrid secret", "") - } else { - // Just print instructions for right now - fmt.Println("Please visit https://pangolin.fossorial.io, create a self hosted node, and return with the credentials.") - } + config.HybridId = readString(reader, "Enter your ID", "") + config.HybridSecret = readString(reader, "Enter your secret", "") + } config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "") config.InstallGerbil = true @@ -620,11 +611,6 @@ func checkPortsAvailable(port int) error { } func checkIsPangolinInstalledWithHybrid() bool { - // Check if docker-compose.yml exists (indicating Pangolin is installed) - if _, err := os.Stat("docker-compose.yml"); err != nil { - return false - } - // Check if config/config.yml exists and contains hybrid section if _, err := os.Stat("config/config.yml"); err != nil { return false diff --git a/install/quickStart.go b/install/quickStart.go new file mode 100644 index 00000000..0904ce86 --- /dev/null +++ b/install/quickStart.go @@ -0,0 +1,109 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const ( + FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e" + // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" + CLOUD_API_URL = "http://localhost:4000/api/v1/remote-exit-node/quick-start" +) + +// HybridCredentials represents the response from the cloud API +type HybridCredentials struct { + RemoteExitNodeId string `json:"remoteExitNodeId"` + Secret string `json:"secret"` +} + +// APIResponse represents the full response structure from the cloud API +type APIResponse struct { + Data HybridCredentials `json:"data"` +} + +// RequestPayload represents the request body structure +type RequestPayload struct { + Token string `json:"token"` +} + +func generateValidationToken() string { + timestamp := time.Now().UnixMilli() + data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp) + obfuscated := make([]byte, len(data)) + for i, char := range []byte(data) { + obfuscated[i] = char + 5 + } + return base64.StdEncoding.EncodeToString(obfuscated) +} + +// requestHybridCredentials makes an HTTP POST request to the cloud API +// to get hybrid credentials (ID and secret) +func requestHybridCredentials() (*HybridCredentials, error) { + // Generate validation token + token := generateValidationToken() + + // Create request payload + payload := RequestPayload{ + Token: token, + } + + // Marshal payload to JSON + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal request payload: %v", err) + } + + // Create HTTP request + req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %v", err) + } + + // Set headers + req.Header.Set("Content-Type", "application/json") + + // Create HTTP client with timeout + client := &http.Client{ + Timeout: 30 * time.Second, + } + + // Make the request + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make HTTP request: %v", err) + } + defer resp.Body.Close() + + // Check response status + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + // Read response body for debugging + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + + // Print the raw JSON response for debugging + // fmt.Printf("Raw JSON response: %s\n", string(body)) + + // Parse response + var apiResponse APIResponse + if err := json.Unmarshal(body, &apiResponse); err != nil { + return nil, fmt.Errorf("failed to decode API response: %v", err) + } + + // Validate response data + if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" { + return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret") + } + + return &apiResponse.Data, nil +} From 77796e8a7523a80dd93dbea01086ee6a874b8387 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 17:48:55 -0700 Subject: [PATCH 193/219] Adjust again for uncertian config --- server/lib/consts.ts | 2 +- server/lib/readConfigFile.ts | 2 +- server/routers/badger/verifySession.ts | 2 +- server/routers/idp/createOidcIdp.ts | 2 +- server/routers/idp/generateOidcUrl.ts | 4 ++-- server/routers/idp/getIdp.ts | 2 +- server/routers/idp/updateOidcIdp.ts | 2 +- server/routers/idp/validateOidcCallback.ts | 4 ++-- server/routers/newt/handleNewtRegisterMessage.ts | 11 ++++++++--- 9 files changed, 18 insertions(+), 13 deletions(-) diff --git a/server/lib/consts.ts b/server/lib/consts.ts index cfe45620..b9afa792 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.8.0"; +export const APP_VERSION = "1.9.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 23098ac9..964e59ec 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -44,7 +44,7 @@ export const configSchema = z name: z.string().optional(), id: z.string().optional(), secret: z.string().optional(), - endpoint: z.string().optional(), + endpoint: z.string().optional().default("https://pangolin.fossorial.io"), redirect_endpoint: z.string().optional() }) .optional(), diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 50b9ed68..1f9ba191 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -195,7 +195,7 @@ export async function verifyResourceSession( if (config.isHybridMode()) { endpoint = config.getRawConfig().hybrid?.redirect_endpoint || config.getRawConfig().hybrid?.endpoint || ""; } else { - endpoint = config.getRawConfig().app.dashboard_url; + endpoint = config.getRawConfig().app.dashboard_url!; } const redirectUrl = `${endpoint}/auth/resource/${encodeURIComponent( resource.resourceId diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index aac5d15e..6078f5aa 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -81,7 +81,7 @@ export async function createOidcIdp( autoProvision } = parsedBody.data; - const key = config.getRawConfig().server.secret; + const key = config.getRawConfig().server.secret!; const encryptedSecret = encrypt(clientSecret, key); const encryptedClientId = encrypt(clientId, key); diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 36c55dcb..c507198a 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -89,7 +89,7 @@ export async function generateOidcUrl( return scope.length > 0; }); - const key = config.getRawConfig().server.secret; + const key = config.getRawConfig().server.secret!; const decryptedClientId = decrypt( existingIdp.idpOidcConfig.clientId, @@ -124,7 +124,7 @@ export async function generateOidcUrl( state, codeVerifier }, - config.getRawConfig().server.secret + config.getRawConfig().server.secret! ); res.cookie("p_oidc_state", stateJwt, { diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts index 227a2429..a202f4ea 100644 --- a/server/routers/idp/getIdp.ts +++ b/server/routers/idp/getIdp.ts @@ -65,7 +65,7 @@ export async function getIdp( return next(createHttpError(HttpCode.NOT_FOUND, "Idp not found")); } - const key = config.getRawConfig().server.secret; + const key = config.getRawConfig().server.secret!; if (idpRes.idp.type === "oidc") { const clientSecret = idpRes.idpOidcConfig!.clientSecret; diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 58771b33..904d0d9e 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -119,7 +119,7 @@ export async function updateOidcIdp( ); } - const key = config.getRawConfig().server.secret; + const key = config.getRawConfig().server.secret!; const encryptedSecret = clientSecret ? encrypt(clientSecret, key) : undefined; diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 3a643386..67e2baad 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -96,7 +96,7 @@ export async function validateOidcCallback( ); } - const key = config.getRawConfig().server.secret; + const key = config.getRawConfig().server.secret!; const decryptedClientId = decrypt( existingIdp.idpOidcConfig.clientId, @@ -116,7 +116,7 @@ export async function validateOidcCallback( const statePayload = jsonwebtoken.verify( storedState, - config.getRawConfig().server.secret, + config.getRawConfig().server.secret!, function (err, decoded) { if (err) { logger.error("Error verifying state JWT", { err }); diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 2ffc7e1f..3c7ecaff 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -71,8 +71,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { .where(eq(sites.siteId, siteId)) .limit(1); - if (!oldSite || !oldSite.exitNodeId) { - logger.warn("Site not found or does not have exit node"); + if (!oldSite) { + logger.warn("Site not found"); return; } @@ -137,13 +137,18 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { .returning(); } + if (!exitNodeIdToQuery) { + logger.warn("No exit node ID to query"); + return; + } + const [exitNode] = await db .select() .from(exitNodes) .where(eq(exitNodes.exitNodeId, exitNodeIdToQuery)) .limit(1); - if (oldSite.pubKey && oldSite.pubKey !== publicKey) { + if (oldSite.pubKey && oldSite.pubKey !== publicKey && oldSite.exitNodeId) { logger.info("Public key mismatch. Deleting old peer..."); await deletePeer(oldSite.exitNodeId, oldSite.pubKey); } From 49cb2ae26062c7be8ee1bfa6e1b270e0d703ec8c Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 18:49:58 -0700 Subject: [PATCH 194/219] Fixes for siteResources with clients --- server/routers/client/targets.ts | 12 ++--- server/routers/newt/handleGetConfigMessage.ts | 29 ++++------- .../siteResource/createSiteResource.ts | 4 +- .../siteResource/deleteSiteResource.ts | 3 +- .../siteResource/updateSiteResource.ts | 3 +- server/routers/traefik/getTraefikConfig.ts | 50 ++++++++++--------- .../CreateInternalResourceDialog.tsx | 2 +- src/components/EditInternalResourceDialog.tsx | 2 +- 8 files changed, 48 insertions(+), 57 deletions(-) diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index 8d13d8cf..e34a23e9 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -5,11 +5,9 @@ export async function addTargets( destinationIp: string, destinationPort: number, protocol: string, - port: number | null = null + port: number ) { - const target = `${port ? port + ":" : ""}${ - destinationIp - }:${destinationPort}`; + const target = `${port}:${destinationIp}:${destinationPort}`; await sendToClient(newtId, { type: `newt/wg/${protocol}/add`, @@ -24,11 +22,9 @@ export async function removeTargets( destinationIp: string, destinationPort: number, protocol: string, - port: number | null = null + port: number ) { - const target = `${port ? port + ":" : ""}${ - destinationIp - }:${destinationPort}`; + const target = `${port}:${destinationIp}:${destinationPort}`; await sendToClient(newtId, { type: `newt/wg/${protocol}/remove`, diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 179c3953..b6206064 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -7,6 +7,7 @@ import { ExitNode, exitNodes, resources, + siteResources, Target, targets } from "@server/db"; @@ -208,33 +209,23 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { const validPeers = peers.filter((peer) => peer !== null); // Get all enabled targets with their resource protocol information - const allTargets = await db - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled, - protocol: resources.protocol - }) - .from(targets) - .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) - .where(and(eq(targets.siteId, siteId), eq(targets.enabled, true))); + const allSiteResources = await db + .select() + .from(siteResources) + .where(eq(siteResources.siteId, siteId)); - const { tcpTargets, udpTargets } = allTargets.reduce( - (acc, target) => { + const { tcpTargets, udpTargets } = allSiteResources.reduce( + (acc, resource) => { // Filter out invalid targets - if (!target.internalPort || !target.ip || !target.port) { + if (!resource.proxyPort || !resource.destinationIp || !resource.destinationPort) { return acc; } // Format target into string - const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`; + const formattedTarget = `${resource.proxyPort}:${resource.destinationIp}:${resource.destinationPort}`; // Add to the appropriate protocol array - if (target.protocol === "tcp") { + if (resource.protocol === "tcp") { acc.tcpTargets.push(formattedTarget); } else { acc.udpTargets.push(formattedTarget); diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 4d80c7a0..da41c19c 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -24,7 +24,7 @@ const createSiteResourceSchema = z protocol: z.enum(["tcp", "udp"]), proxyPort: z.number().int().positive(), destinationPort: z.number().int().positive(), - destinationIp: z.string().ip(), + destinationIp: z.string(), enabled: z.boolean().default(true) }) .strict(); @@ -146,7 +146,7 @@ export async function createSiteResource( return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); } - await addTargets(newt.newtId, destinationIp, destinationPort, protocol); + await addTargets(newt.newtId, destinationIp, destinationPort, protocol, proxyPort); logger.info( `Created site resource ${newSiteResource.siteResourceId} for site ${siteId}` diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index df29faf5..347d4b53 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -105,7 +105,8 @@ export async function deleteSiteResource( newt.newtId, existingSiteResource.destinationIp, existingSiteResource.destinationPort, - existingSiteResource.protocol + existingSiteResource.protocol, + existingSiteResource.proxyPort ); logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`); diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index bd717463..82e2fe68 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -170,7 +170,8 @@ export async function updateSiteResource( newt.newtId, updatedSiteResource.destinationIp, updatedSiteResource.destinationPort, - updatedSiteResource.protocol + updatedSiteResource.protocol, + updatedSiteResource.proxyPort ); logger.info( diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 311542e1..452c7228 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -54,29 +54,31 @@ export async function traefikConfigProvider( config.getRawConfig().traefik.site_types ); - traefikConfig.http.middlewares[badgerMiddlewareName] = { - plugin: { - [badgerMiddlewareName]: { - apiBaseUrl: new URL( - "/api/v1", - `http://${ - config.getRawConfig().server.internal_hostname - }:${config.getRawConfig().server.internal_port}` - ).href, - userSessionCookieName: - config.getRawConfig().server.session_cookie_name, + if (traefikConfig?.http?.middlewares) { // BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING + traefikConfig.http.middlewares[badgerMiddlewareName] = { + plugin: { + [badgerMiddlewareName]: { + apiBaseUrl: new URL( + "/api/v1", + `http://${ + config.getRawConfig().server.internal_hostname + }:${config.getRawConfig().server.internal_port}` + ).href, + userSessionCookieName: + config.getRawConfig().server.session_cookie_name, - // deprecated - accessTokenQueryParam: - config.getRawConfig().server - .resource_access_token_param, + // deprecated + accessTokenQueryParam: + config.getRawConfig().server + .resource_access_token_param, - resourceSessionRequestParam: - config.getRawConfig().server - .resource_session_request_param + resourceSessionRequestParam: + config.getRawConfig().server + .resource_session_request_param + } } - } - }; + }; + } return res.status(HttpCode.OK).json(traefikConfig); } catch (e) { @@ -320,11 +322,11 @@ export async function getTraefikConfig( loadBalancer: { servers: (() => { // Check if any sites are online - // THIS IS SO THAT THERE IS SOME IMMEDIATE FEEDBACK + // THIS IS SO THAT THERE IS SOME IMMEDIATE FEEDBACK // EVEN IF THE SITES HAVE NOT UPDATED YET FROM THE - // RECEIVE BANDWIDTH ENDPOINT. - - // TODO: HOW TO HANDLE ^^^^^^ BETTER + // RECEIVE BANDWIDTH ENDPOINT. + + // TODO: HOW TO HANDLE ^^^^^^ BETTER const anySitesOnline = ( targets as TargetWithSite[] ).some((target: TargetWithSite) => target.site.online); diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 3c4841d7..ccfddcd8 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -87,7 +87,7 @@ export default function CreateInternalResourceDialog({ .positive() .min(1, t("createInternalResourceDialogProxyPortMin")) .max(65535, t("createInternalResourceDialogProxyPortMax")), - destinationIp: z.string().ip(t("createInternalResourceDialogInvalidIPAddressFormat")), + destinationIp: z.string(), destinationPort: z .number() .int() diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 5d594d02..adfed1b7 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -73,7 +73,7 @@ export default function EditInternalResourceDialog({ name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), protocol: z.enum(["tcp", "udp"]), proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")), - destinationIp: z.string().ip(t("editInternalResourceDialogInvalidIPAddressFormat")), + destinationIp: z.string(), destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")) }); From c53eac76f8e7d88eb3a1aa0087d982cf3b775372 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 18:50:39 -0700 Subject: [PATCH 195/219] Bug fixes around hybrid --- install/main.go | 2 +- install/quickStart.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/install/main.go b/install/main.go index 3e9c093c..458edc2c 100644 --- a/install/main.go +++ b/install/main.go @@ -309,7 +309,7 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") for { - response := readString(reader, "Do you want to install Pangolin as a cloud-managed self-hosted node? (yes/no)", "") + response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "") if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") { config.HybridMode = true break diff --git a/install/quickStart.go b/install/quickStart.go index 0904ce86..28a50ed6 100644 --- a/install/quickStart.go +++ b/install/quickStart.go @@ -13,7 +13,7 @@ import ( const ( FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e" // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" - CLOUD_API_URL = "http://localhost:4000/api/v1/remote-exit-node/quick-start" + CLOUD_API_URL = "https://enterprise.fosrl.io/api/v1/remote-exit-node/quick-start" ) // HybridCredentials represents the response from the cloud API @@ -67,6 +67,7 @@ func requestHybridCredentials() (*HybridCredentials, error) { // Set headers req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-CSRF-Token", "x-csrf-protection") // Create HTTP client with timeout client := &http.Client{ From ec0b6b64fe9843b519a0ce1f44ded033b83ed4c0 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 20 Aug 2025 23:16:40 -0700 Subject: [PATCH 196/219] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index c91c7ed8..e22c52a5 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -12,7 +12,7 @@ "componentsErrorNoMember": "В момента не сте част от организация.", "welcome": "Добре дошли!", "welcomeTo": "Добре дошли в", - "componentsCreateOrg": "Създай организация", + "componentsCreateOrg": "Създаване на организация", "componentsMember": "Вие сте част от {count, plural, =0 {нула организации} one {една организация} other {# организации}}.", "componentsInvalidKey": "Засечен е невалиден или изтекъл лиценз. Проверете лицензионните условия, за да се възползвате от всички функционалности.", "dismiss": "Отхвърляне", From d62c35945240191254f64730e53ffe736bd98a74 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 21 Aug 2025 00:53:41 -0700 Subject: [PATCH 197/219] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index e22c52a5..9b6686b5 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -1,8 +1,8 @@ { "setupCreate": "Създайте своя организация, сайт и ресурси", "setupNewOrg": "Нова организация", - "setupCreateOrg": "Създай организация", - "setupCreateResources": "Създай ресурси", + "setupCreateOrg": "Създаване на организация", + "setupCreateResources": "Създаване на ресурси", "setupOrgName": "Име на организацията", "orgDisplayName": "Това е публичното име на вашата организация.", "orgId": "Идентификатор на организация", From 5ff5660db3007cf5d95dfa4a9618c6f1c3926509 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 21 Aug 2025 14:12:09 -0700 Subject: [PATCH 198/219] Add key --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 208143c6..95b1b9be 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ tsconfig.tsbuildinfo config/config.yml config/postgres config/postgres* +config/openapi.yaml +config/key dist .dist installer @@ -38,7 +40,6 @@ test_event.json .idea/ public/branding server/db/index.ts -config/openapi.yaml server/build.ts postgres/ dynamic/ From 60d883139900c4176a38d3cdfbb8ac5ea72cda77 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 21 Aug 2025 14:17:38 -0700 Subject: [PATCH 199/219] Rename hybrid to managed --- install/config/config.yml | 2 +- install/main.go | 2 +- server/auth/sessions/resource.ts | 4 ++-- server/db/queries/verifySessionQueries.ts | 24 +++++++++---------- server/emails/index.ts | 2 +- server/hybridServer.ts | 2 +- server/index.ts | 2 +- server/lib/config.ts | 8 +++---- server/lib/readConfigFile.ts | 8 +++---- server/lib/remoteCertificates/certificates.ts | 2 +- server/lib/remoteProxy.ts | 2 +- server/lib/tokenManager.ts | 2 +- server/lib/traefikConfig.ts | 6 ++--- server/routers/badger/verifySession.ts | 4 ++-- server/routers/gerbil/getConfig.ts | 2 +- server/routers/internal.ts | 4 ++-- server/setup/ensureSetupToken.ts | 2 +- 17 files changed, 39 insertions(+), 39 deletions(-) diff --git a/install/config/config.yml b/install/config/config.yml index 0dde388a..5a86a930 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -5,7 +5,7 @@ gerbil: start_port: 51820 base_endpoint: "{{.DashboardDomain}}" {{if .HybridMode}} -hybrid: +managed: id: "{{.HybridId}}" secret: "{{.HybridSecret}}" diff --git a/install/main.go b/install/main.go index 458edc2c..1d684b51 100644 --- a/install/main.go +++ b/install/main.go @@ -623,5 +623,5 @@ func checkIsPangolinInstalledWithHybrid() bool { } // Check for hybrid section - return bytes.Contains(content, []byte("hybrid:")) + return bytes.Contains(content, []byte("managed:")) } diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index 8d676bec..511dadda 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -65,9 +65,9 @@ export async function validateResourceSessionToken( token: string, resourceId: number ): Promise { - if (config.isHybridMode()) { + if (config.isManagedMode()) { try { - const response = await axios.post(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, { + const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, { token: token }, await tokenManager.getAuthHeader()); return response.data.data; diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 4c800125..728880f2 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -37,9 +37,9 @@ export type UserSessionWithUser = { export async function getResourceByDomain( domain: string ): Promise { - if (config.isHybridMode()) { + if (config.isManagedMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/resource/domain/${domain}`, await tokenManager.getAuthHeader()); + const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`, await tokenManager.getAuthHeader()); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { @@ -89,9 +89,9 @@ export async function getResourceByDomain( export async function getUserSessionWithUser( userSessionId: string ): Promise { - if (config.isHybridMode()) { + if (config.isManagedMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/session/${userSessionId}`, await tokenManager.getAuthHeader()); + const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`, await tokenManager.getAuthHeader()); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { @@ -130,9 +130,9 @@ export async function getUserSessionWithUser( * Get user organization role */ export async function getUserOrgRole(userId: string, orgId: string) { - if (config.isHybridMode()) { + if (config.isManagedMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`, await tokenManager.getAuthHeader()); + const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`, await tokenManager.getAuthHeader()); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { @@ -169,9 +169,9 @@ export async function getUserOrgRole(userId: string, orgId: string) { * Check if role has access to resource */ export async function getRoleResourceAccess(resourceId: number, roleId: number) { - if (config.isHybridMode()) { + if (config.isManagedMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader()); + const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader()); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { @@ -208,9 +208,9 @@ export async function getRoleResourceAccess(resourceId: number, roleId: number) * Check if user has direct access to resource */ export async function getUserResourceAccess(userId: string, resourceId: number) { - if (config.isHybridMode()) { + if (config.isManagedMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader()); + const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader()); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { @@ -247,9 +247,9 @@ export async function getUserResourceAccess(userId: string, resourceId: number) * Get resource rules for a given resource */ export async function getResourceRules(resourceId: number): Promise { - if (config.isHybridMode()) { + if (config.isManagedMode()) { try { - const response = await axios.get(`${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`, await tokenManager.getAuthHeader()); + const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`, await tokenManager.getAuthHeader()); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { diff --git a/server/emails/index.ts b/server/emails/index.ts index 0388d3bb..2cdef8a1 100644 --- a/server/emails/index.ts +++ b/server/emails/index.ts @@ -6,7 +6,7 @@ import logger from "@server/logger"; import SMTPTransport from "nodemailer/lib/smtp-transport"; function createEmailClient() { - if (config.isHybridMode()) { + if (config.isManagedMode()) { // LETS NOT WORRY ABOUT EMAILS IN HYBRID return; } diff --git a/server/hybridServer.ts b/server/hybridServer.ts index 2cd04e0d..bb26489d 100644 --- a/server/hybridServer.ts +++ b/server/hybridServer.ts @@ -23,7 +23,7 @@ export async function createHybridClientServer() { // Create client const client = createWebSocketClient( token, - config.getRawConfig().hybrid!.endpoint!, + config.getRawConfig().managed!.endpoint!, { reconnectInterval: 5000, pingInterval: 30000, diff --git a/server/index.ts b/server/index.ts index c8aaff73..746de7b9 100644 --- a/server/index.ts +++ b/server/index.ts @@ -27,7 +27,7 @@ async function startServers() { let hybridClientServer; let nextServer; - if (config.isHybridMode()) { + if (config.isManagedMode()) { hybridClientServer = await createHybridClientServer(); } else { nextServer = await createNextServer(); diff --git a/server/lib/config.ts b/server/lib/config.ts index 2437eaac..667df744 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -96,8 +96,8 @@ export class Config { if (!this.rawConfig) { throw new Error("Config not loaded. Call load() first."); } - if (this.rawConfig.hybrid) { - // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID + if (this.rawConfig.managed) { + // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED return; } license.setServerSecret(this.rawConfig.server.secret!); @@ -149,8 +149,8 @@ export class Config { return false; } - public isHybridMode() { - return typeof this.rawConfig?.hybrid === "object"; + public isManagedMode() { + return typeof this.rawConfig?.managed === "object"; } public async checkSupporterKey() { diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 964e59ec..79ebd999 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -39,7 +39,7 @@ export const configSchema = z anonymous_usage: true } }), - hybrid: z + managed: z .object({ name: z.string().optional(), id: z.string().optional(), @@ -306,7 +306,7 @@ export const configSchema = z return true; } // If hybrid is defined, domains are not required - if (data.hybrid) { + if (data.managed) { return true; } if (keys.length === 0) { @@ -321,7 +321,7 @@ export const configSchema = z .refine( (data) => { // If hybrid is defined, server secret is not required - if (data.hybrid) { + if (data.managed) { return true; } // If hybrid is not defined, server secret must be defined @@ -334,7 +334,7 @@ export const configSchema = z .refine( (data) => { // If hybrid is defined, dashboard_url is not required - if (data.hybrid) { + if (data.managed) { return true; } // If hybrid is not defined, dashboard_url must be defined diff --git a/server/lib/remoteCertificates/certificates.ts b/server/lib/remoteCertificates/certificates.ts index f9d98e93..db6fa6ad 100644 --- a/server/lib/remoteCertificates/certificates.ts +++ b/server/lib/remoteCertificates/certificates.ts @@ -24,7 +24,7 @@ export async function getValidCertificatesForDomainsHybrid(domains: Set) try { const response = await axios.get( - `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/certificates/domains`, + `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`, { params: { domains: domainArray diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts index 3b9dcd69..2dad9ba8 100644 --- a/server/lib/remoteProxy.ts +++ b/server/lib/remoteProxy.ts @@ -18,7 +18,7 @@ export const proxyToRemote = async ( endpoint: string ): Promise => { try { - const remoteUrl = `${config.getRawConfig().hybrid?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`; + const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`; logger.debug(`Proxying request to remote server: ${remoteUrl}`); diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts index 45f280ba..2e0e1118 100644 --- a/server/lib/tokenManager.ts +++ b/server/lib/tokenManager.ts @@ -174,7 +174,7 @@ export class TokenManager { this.isRefreshing = true; try { - const hybridConfig = config.getRawConfig().hybrid; + const hybridConfig = config.getRawConfig().managed; if ( !hybridConfig?.id || diff --git a/server/lib/traefikConfig.ts b/server/lib/traefikConfig.ts index a168ea0b..7f4289a4 100644 --- a/server/lib/traefikConfig.ts +++ b/server/lib/traefikConfig.ts @@ -281,7 +281,7 @@ export class TraefikConfigManager { if (this.shouldFetchCertificates(domains)) { // Get valid certificates for active domains - if (config.isHybridMode()) { + if (config.isManagedMode()) { validCertificates = await getValidCertificatesForDomainsHybrid(domains); } else { @@ -383,9 +383,9 @@ export class TraefikConfigManager { } | null> { let traefikConfig; try { - if (config.isHybridMode()) { + if (config.isManagedMode()) { const resp = await axios.get( - `${config.getRawConfig().hybrid?.endpoint}/api/v1/hybrid/traefik-config`, + `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`, await tokenManager.getAuthHeader() ); diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 1f9ba191..79951cb5 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -192,8 +192,8 @@ export async function verifyResourceSession( } let endpoint: string; - if (config.isHybridMode()) { - endpoint = config.getRawConfig().hybrid?.redirect_endpoint || config.getRawConfig().hybrid?.endpoint || ""; + if (config.isManagedMode()) { + endpoint = config.getRawConfig().managed?.redirect_endpoint || config.getRawConfig().managed?.endpoint || ""; } else { endpoint = config.getRawConfig().app.dashboard_url!; } diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 7cf69245..f7663f53 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -104,7 +104,7 @@ export async function getConfig( } // STOP HERE IN HYBRID MODE - if (config.isHybridMode()) { + if (config.isManagedMode()) { req.body = { ...req.body, endpoint: exitNode[0].endpoint, diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 805e284f..b961ef6f 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -51,7 +51,7 @@ internalRouter.get("/idp/:idpId", idp.getIdp); const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); -if (config.isHybridMode()) { +if (config.isManagedMode()) { // Use proxy router to forward requests to remote cloud server // Proxy endpoints for each gerbil route gerbilRouter.post("/receive-bandwidth", (req, res, next) => @@ -90,7 +90,7 @@ internalRouter.use("/badger", badgerRouter); badgerRouter.post("/verify-session", badger.verifyResourceSession); -if (config.isHybridMode()) { +if (config.isManagedMode()) { badgerRouter.post("/exchange-session", (req, res, next) => proxyToRemote(req, res, next, "hybrid/badger/exchange-session") ); diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 49608218..078c99ee 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -23,7 +23,7 @@ function generateId(length: number): string { } export async function ensureSetupToken() { - if (config.isHybridMode()) { + if (config.isManagedMode()) { // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID return; } From 01b3b1971594f26bf4e94206b2ec240a1d678520 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Fri, 22 Aug 2025 07:34:03 -0700 Subject: [PATCH 200/219] New translations en-us.json (Czech) --- messages/cs-CZ.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 7c4fd609..1e8ef2e7 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -10,8 +10,8 @@ "setupErrorIdentifier": "ID organizace je již použito. Zvolte prosím jiné.", "componentsErrorNoMemberCreate": "Zatím nejste členem žádné organizace. Abyste mohli začít, vytvořte si organizaci.", "componentsErrorNoMember": "Zatím nejste členem žádných organizací.", - "welcome": "Welcome!", - "welcomeTo": "Welcome to", + "welcome": "Vítejte!", + "welcomeTo": "Vítejte v", "componentsCreateOrg": "Vytvořte organizaci", "componentsMember": "Jste členem {count, plural, =0 {0 organizací} one {1 organizace} other {# organizací}}.", "componentsInvalidKey": "Byly nalezeny neplatné nebo propadlé licenční klíče. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.", @@ -62,15 +62,15 @@ "method": "Způsob", "siteMethodDescription": "Tímto způsobem budete vystavovat spojení.", "siteLearnNewt": "Naučte se, jak nainstalovat Newt na svůj systém", - "siteSeeConfigOnce": "You will only be able to see the configuration once.", - "siteLoadWGConfig": "Loading WireGuard configuration...", - "siteDocker": "Expand for Docker Deployment Details", - "toggle": "Toggle", + "siteSeeConfigOnce": "Konfiguraci uvidíte pouze jednou.", + "siteLoadWGConfig": "Načítání konfigurace WireGuard...", + "siteDocker": "Rozbalit pro detaily nasazení v Dockeru", + "toggle": "Přepínač", "dockerCompose": "Docker Compose", "dockerRun": "Docker Run", - "siteLearnLocal": "Local sites do not tunnel, learn more", - "siteConfirmCopy": "I have copied the config", - "searchSitesProgress": "Search sites...", + "siteLearnLocal": "Místní lokality se netunelují, dozvědět se více", + "siteConfirmCopy": "Konfiguraci jsem zkopíroval", + "searchSitesProgress": "Hledat lokality...", "siteAdd": "Add Site", "siteInstallNewt": "Install Newt", "siteInstallNewtDescription": "Get Newt running on your system", From a66613c5caa6e1e660e67fe5dcb299cab438fc95 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Fri, 22 Aug 2025 09:05:46 -0700 Subject: [PATCH 201/219] New translations en-us.json (Czech) --- messages/cs-CZ.json | 154 ++++++++++++++++++++++---------------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 1e8ef2e7..6c25055c 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -71,84 +71,84 @@ "siteLearnLocal": "Místní lokality se netunelují, dozvědět se více", "siteConfirmCopy": "Konfiguraci jsem zkopíroval", "searchSitesProgress": "Hledat lokality...", - "siteAdd": "Add Site", - "siteInstallNewt": "Install Newt", - "siteInstallNewtDescription": "Get Newt running on your system", - "WgConfiguration": "WireGuard Configuration", - "WgConfigurationDescription": "Use the following configuration to connect to your network", - "operatingSystem": "Operating System", - "commands": "Commands", - "recommended": "Recommended", - "siteNewtDescription": "For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard.", - "siteRunsInDocker": "Runs in Docker", - "siteRunsInShell": "Runs in shell on macOS, Linux, and Windows", - "siteErrorDelete": "Error deleting site", - "siteErrorUpdate": "Failed to update site", - "siteErrorUpdateDescription": "An error occurred while updating the site.", - "siteUpdated": "Site updated", - "siteUpdatedDescription": "The site has been updated.", - "siteGeneralDescription": "Configure the general settings for this site", - "siteSettingDescription": "Configure the settings on your site", - "siteSetting": "{siteName} Settings", - "siteNewtTunnel": "Newt Tunnel (Recommended)", - "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", - "siteWg": "Basic WireGuard", - "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", - "siteLocalDescription": "Local resources only. No tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", - "siteSeeAll": "See All Sites", - "siteTunnelDescription": "Determine how you want to connect to your site", - "siteNewtCredentials": "Newt Credentials", - "siteNewtCredentialsDescription": "This is how Newt will authenticate with the server", - "siteCredentialsSave": "Save Your Credentials", - "siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "siteInfo": "Site Information", - "status": "Status", - "shareTitle": "Manage Share Links", - "shareDescription": "Create shareable links to grant temporary or permanent access to your resources", - "shareSearch": "Search share links...", - "shareCreate": "Create Share Link", - "shareErrorDelete": "Failed to delete link", - "shareErrorDeleteMessage": "An error occurred deleting link", - "shareDeleted": "Link deleted", - "shareDeletedDescription": "The link has been deleted", - "shareTokenDescription": "Your access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.", - "accessToken": "Access Token", - "usageExamples": "Usage Examples", - "tokenId": "Token ID", - "requestHeades": "Request Headers", - "queryParameter": "Query Parameter", - "importantNote": "Important Note", - "shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.", + "siteAdd": "Přidat lokalitu", + "siteInstallNewt": "Nainstalovat Newt", + "siteInstallNewtDescription": "Spustit Newt na vašem systému", + "WgConfiguration": "Konfigurace WireGuard", + "WgConfigurationDescription": "Použijte následující konfiguraci pro připojení k vaší síti", + "operatingSystem": "Operační systém", + "commands": "Příkazy", + "recommended": "Doporučeno", + "siteNewtDescription": "Ideálně použijte Newt, který využívá WireGuard a umožňuje adresovat vaše soukromé zdroje pomocí jejich LAN adresy ve vaší privátní síti přímo z dashboardu Pangolin.", + "siteRunsInDocker": "Běží v Dockeru", + "siteRunsInShell": "Běží v shellu na macOS, Linuxu a Windows", + "siteErrorDelete": "Chyba při odstraňování lokality", + "siteErrorUpdate": "Nepodařilo se upravit lokalitu", + "siteErrorUpdateDescription": "Při úpravě lokality došlo k chybě.", + "siteUpdated": "Lokalita upravena", + "siteUpdatedDescription": "Lokalita byla upravena.", + "siteGeneralDescription": "Upravte obecná nastavení pro tuto lokalitu", + "siteSettingDescription": "Upravte nastavení vaší lokality", + "siteSetting": "Nastavení {siteName}", + "siteNewtTunnel": "Tunel Newt (doporučeno)", + "siteNewtTunnelDescription": "Nejjednodušší způsob, jak vytvořit vstupní bod do vaší sítě. Žádné další nastavení.", + "siteWg": "Základní WireGuard", + "siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.", + "siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH", + "siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.", + "siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH", + "siteSeeAll": "Zobrazit všechny lokality", + "siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě", + "siteNewtCredentials": "Přihlašovací údaje Newt", + "siteNewtCredentialsDescription": "Tímto způsobem se bude Newt autentizovat na serveru", + "siteCredentialsSave": "Uložit přihlašovací údaje", + "siteCredentialsSaveDescription": "Toto nastavení uvidíte pouze jednou. Ujistěte se, že jej zkopírujete na bezpečné místo.", + "siteInfo": "Údaje o lokalitě", + "status": "Stav", + "shareTitle": "Spravovat sdílení odkazů", + "shareDescription": "Vytvořte odkazy, abyste udělili dočasný nebo trvalý přístup k vašim zdrojům", + "shareSearch": "Hledat sdílené odkazy...", + "shareCreate": "Vytvořit odkaz", + "shareErrorDelete": "Nepodařilo se odstranit odkaz", + "shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu", + "shareDeleted": "Odkaz odstraněn", + "shareDeletedDescription": "Odkaz byl odstraněn", + "shareTokenDescription": "Váš přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientem v každé žádosti o ověřený přístup.", + "accessToken": "Přístupový token", + "usageExamples": "Příklady použití", + "tokenId": "ID tokenu", + "requestHeades": "Hlavičky požadavku", + "queryParameter": "Parametry dotazu", + "importantNote": "Důležité upozornění", + "shareImportantDescription": "Z bezpečnostních důvodů je doporučeno používat raději hlavičky než parametry dotazu pokud je to možné, protože parametry dotazu mohou být zaznamenány v logu serveru nebo v historii prohlížeče.", "token": "Token", - "shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.", - "shareErrorFetchResource": "Failed to fetch resources", - "shareErrorFetchResourceDescription": "An error occurred while fetching the resources", - "shareErrorCreate": "Failed to create share link", - "shareErrorCreateDescription": "An error occurred while creating the share link", - "shareCreateDescription": "Anyone with this link can access the resource", - "shareTitleOptional": "Title (optional)", - "expireIn": "Expire In", - "neverExpire": "Never expire", - "shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.", - "shareSeeOnce": "You will only be able to see this linkonce. Make sure to copy it.", - "shareAccessHint": "Anyone with this link can access the resource. Share it with care.", - "shareTokenUsage": "See Access Token Usage", - "createLink": "Create Link", - "resourcesNotFound": "No resources found", - "resourceSearch": "Search resources", - "openMenu": "Open menu", - "resource": "Resource", - "title": "Title", - "created": "Created", - "expires": "Expires", - "never": "Never", - "shareErrorSelectResource": "Please select a resource", - "resourceTitle": "Manage Resources", - "resourceDescription": "Create secure proxies to your private applications", - "resourcesSearch": "Search resources...", - "resourceAdd": "Add Resource", + "shareTokenSecurety": "Uchovejte přístupový token v bezpečí. Nesdílejte jej na veřejně přístupných místěch nebo v kódu na straně klienta.", + "shareErrorFetchResource": "Nepodařilo se načíst zdroje", + "shareErrorFetchResourceDescription": "Při načítání zdrojů došlo k chybě", + "shareErrorCreate": "Nepodařilo se vytvořit odkaz", + "shareErrorCreateDescription": "Při vytváření odkazu došlo k chybě", + "shareCreateDescription": "Kdokoliv s tímto odkazem může přistupovat ke zdroji", + "shareTitleOptional": "Název (volitelné)", + "expireIn": "Platnost vyprší za", + "neverExpire": "Nikdy nevyprší", + "shareExpireDescription": "Doba platnosti určuje, jak dlouho bude odkaz použitelný a bude poskytovat přístup ke zdroji. Po této době odkaz již nebude fungovat a uživatelé kteří tento odkaz používali ztratí přístup ke zdroji.", + "shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Ujistěte se, že jste jej zkopírovali.", + "shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.", + "shareTokenUsage": "Zobrazit využití přístupového tokenu", + "createLink": "Vytvořit odkaz", + "resourcesNotFound": "Nebyly nalezeny žádné zdroje", + "resourceSearch": "Vyhledat zdroje", + "openMenu": "Otevřít nabídku", + "resource": "Zdroj", + "title": "Název", + "created": "Vytvořeno", + "expires": "Vyprší", + "never": "Nikdy", + "shareErrorSelectResource": "Zvolte prosím zdroj", + "resourceTitle": "Spravovat zdroje", + "resourceDescription": "Vytvořte bezpečné proxy služby pro přístup k privátním aplikacím", + "resourcesSearch": "Prohledat zdroje...", + "resourceAdd": "Přidat zdroj", "resourceErrorDelte": "Error deleting resource", "authentication": "Authentication", "protected": "Protected", From 574be52b847dc24f7ccaf477dd8020e3f51af488 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 22 Aug 2025 10:43:04 -0700 Subject: [PATCH 202/219] Revert b4be620a5bdcaa218dd04cdf1dcc602b15abba5c --- .../[orgId]/settings/resources/ResourcesTable.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index 4ec49e9e..97bdfdd9 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -665,21 +665,22 @@ export default function ResourcesTable({ className="w-full" onValueChange={handleTabChange} > - -
-
{getSearchInput()}
+ +
+ {getSearchInput()} + {env.flags.enableClients && ( - - + + {t("resourcesTableProxyResources")} - + {t("resourcesTableClientResources")} )}
-
+
{getActionButton()}
From 04077c53fd2b5d375bf2ac2b40e7f46685933163 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 23 Aug 2025 12:31:16 -0700 Subject: [PATCH 203/219] Update url to cloud --- install/quickStart.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/quickStart.go b/install/quickStart.go index 28a50ed6..ece8e8ff 100644 --- a/install/quickStart.go +++ b/install/quickStart.go @@ -13,7 +13,7 @@ import ( const ( FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e" // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" - CLOUD_API_URL = "https://enterprise.fosrl.io/api/v1/remote-exit-node/quick-start" + CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" ) // HybridCredentials represents the response from the cloud API From 5df87641a1cbc7d86ac7dbc4c8361f447ca1693d Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 23 Aug 2025 15:13:44 -0700 Subject: [PATCH 204/219] Fix #1321 --- server/routers/site/listSites.ts | 85 ++++++++++++++----- .../[orgId]/settings/clients/create/page.tsx | 29 +++++-- .../[orgId]/settings/sites/create/page.tsx | 29 +++++-- 3 files changed, 102 insertions(+), 41 deletions(-) diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index bdf44026..b2655ff6 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -21,11 +21,22 @@ async function getLatestNewtVersion(): Promise { return cachedVersion; } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 1500); // Reduced timeout to 1.5 seconds + const response = await fetch( - "https://api.github.com/repos/fosrl/newt/tags" + "https://api.github.com/repos/fosrl/newt/tags", + { + signal: controller.signal + } ); + + clearTimeout(timeoutId); + if (!response.ok) { - logger.warn("Failed to fetch latest Newt version from GitHub"); + logger.warn( + `Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}` + ); return null; } @@ -40,8 +51,21 @@ async function getLatestNewtVersion(): Promise { newtVersionCache.set("latestNewtVersion", latestVersion); return latestVersion; - } catch (error) { - logger.error("Error fetching latest Newt version:", error); + } catch (error: any) { + if (error.name === "AbortError") { + logger.warn( + "Request to fetch latest Newt version timed out (1.5s)" + ); + } else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { + logger.warn( + "Connection timeout while fetching latest Newt version" + ); + } else { + logger.warn( + "Error fetching latest Newt version:", + error.message || error + ); + } return null; } } @@ -190,33 +214,48 @@ export async function listSites( const totalCountResult = await countQuery; const totalCount = totalCountResult[0].count; - const latestNewtVersion = await getLatestNewtVersion(); + // Get latest version asynchronously without blocking the response + const latestNewtVersionPromise = getLatestNewtVersion(); const sitesWithUpdates: SiteWithUpdateAvailable[] = sitesList.map( (site) => { const siteWithUpdate: SiteWithUpdateAvailable = { ...site }; - - if ( - site.type === "newt" && - site.newtVersion && - latestNewtVersion - ) { - try { - siteWithUpdate.newtUpdateAvailable = semver.lt( - site.newtVersion, - latestNewtVersion - ); - } catch (error) { - siteWithUpdate.newtUpdateAvailable = false; - } - } else { - siteWithUpdate.newtUpdateAvailable = false; - } - + // Initially set to false, will be updated if version check succeeds + siteWithUpdate.newtUpdateAvailable = false; return siteWithUpdate; } ); + // Try to get the latest version, but don't block if it fails + try { + const latestNewtVersion = await latestNewtVersionPromise; + + if (latestNewtVersion) { + sitesWithUpdates.forEach((site) => { + if ( + site.type === "newt" && + site.newtVersion && + latestNewtVersion + ) { + try { + site.newtUpdateAvailable = semver.lt( + site.newtVersion, + latestNewtVersion + ); + } catch (error) { + site.newtUpdateAvailable = false; + } + } + }); + } + } catch (error) { + // Log the error but don't let it block the response + logger.warn( + "Failed to check for Newt updates, continuing without update info:", + error + ); + } + return response(res, { data: { sites: sitesWithUpdates, diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 2d42b82e..0736ee64 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -331,9 +331,16 @@ export default function Page() { let olmVersion = "latest"; try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + const response = await fetch( - `https://api.github.com/repos/fosrl/olm/releases/latest` + `https://api.github.com/repos/fosrl/olm/releases/latest`, + { signal: controller.signal } ); + + clearTimeout(timeoutId); + if (!response.ok) { throw new Error( t("olmErrorFetchReleases", { @@ -345,14 +352,18 @@ export default function Page() { const latestVersion = data.tag_name; olmVersion = latestVersion; } catch (error) { - console.error( - t("olmErrorFetchLatest", { - err: - error instanceof Error - ? error.message - : String(error) - }) - ); + if (error instanceof Error && error.name === 'AbortError') { + console.error(t("olmErrorFetchTimeout")); + } else { + console.error( + t("olmErrorFetchLatest", { + err: + error instanceof Error + ? error.message + : String(error) + }) + ); + } } await api diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 26cba229..b9e172a3 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -488,9 +488,16 @@ WantedBy=default.target` let currentNewtVersion = "latest"; try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + const response = await fetch( - `https://api.github.com/repos/fosrl/newt/releases/latest` + `https://api.github.com/repos/fosrl/newt/releases/latest`, + { signal: controller.signal } ); + + clearTimeout(timeoutId); + if (!response.ok) { throw new Error( t("newtErrorFetchReleases", { @@ -503,14 +510,18 @@ WantedBy=default.target` currentNewtVersion = latestVersion; setNewtVersion(latestVersion); } catch (error) { - console.error( - t("newtErrorFetchLatest", { - err: - error instanceof Error - ? error.message - : String(error) - }) - ); + if (error instanceof Error && error.name === 'AbortError') { + console.error(t("newtErrorFetchTimeout")); + } else { + console.error( + t("newtErrorFetchLatest", { + err: + error instanceof Error + ? error.message + : String(error) + }) + ); + } } const generatedKeypair = generateKeypair(); From 32ba17cf91ba61fad7e206171b3f5612d99bcc93 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 23 Aug 2025 15:26:43 -0700 Subject: [PATCH 205/219] Fix linter errors --- cli/commands/setAdminCredentials.ts | 2 +- server/lib/remoteProxy.ts | 2 +- server/lib/traefikConfig.ts | 2 +- server/routers/badger/exchangeSession.ts | 2 +- server/routers/badger/verifySession.ts | 2 +- server/routers/gerbil/getConfig.ts | 2 +- server/routers/gerbil/updateHolePunch.ts | 2 +- server/routers/olm/handleOlmRegisterMessage.ts | 2 +- server/routers/traefik/getTraefikConfig.ts | 2 +- server/setup/scriptsPg/1.6.0.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta10.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta12.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta15.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta2.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta3.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta5.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta6.ts | 2 +- server/setup/scriptsSqlite/1.0.0-beta9.ts | 2 +- server/setup/scriptsSqlite/1.2.0.ts | 2 +- server/setup/scriptsSqlite/1.3.0.ts | 2 +- server/setup/scriptsSqlite/1.5.0.ts | 2 +- server/setup/scriptsSqlite/1.6.0.ts | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/cli/commands/setAdminCredentials.ts b/cli/commands/setAdminCredentials.ts index c45da602..84a6c795 100644 --- a/cli/commands/setAdminCredentials.ts +++ b/cli/commands/setAdminCredentials.ts @@ -32,7 +32,7 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = { }, handler: async (argv: { email: string; password: string }) => { try { - let { email, password } = argv; + const { email, password } = argv; email = email.trim().toLowerCase(); const parsed = passwordSchema.safeParse(password); diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts index 2dad9ba8..c9016071 100644 --- a/server/lib/remoteProxy.ts +++ b/server/lib/remoteProxy.ts @@ -70,4 +70,4 @@ export const proxyToRemote = async ( ) ); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/server/lib/traefikConfig.ts b/server/lib/traefikConfig.ts index 7f4289a4..03656506 100644 --- a/server/lib/traefikConfig.ts +++ b/server/lib/traefikConfig.ts @@ -142,7 +142,7 @@ export class TraefikConfigManager { const lastUpdateExists = await this.fileExists(lastUpdatePath); let lastModified: Date | null = null; - let expiresAt: Date | null = null; + const expiresAt: Date | null = null; if (lastUpdateExists) { try { diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index b4289281..d6f2c7c7 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -55,7 +55,7 @@ export async function exchangeSession( let cleanHost = host; // if the host ends with :port if (cleanHost.match(/:[0-9]{1,5}$/)) { - let matched = ''+cleanHost.match(/:[0-9]{1,5}$/); + const matched = ''+cleanHost.match(/:[0-9]{1,5}$/); cleanHost = cleanHost.slice(0, -1*matched.length); } diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 79951cb5..905f748d 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -123,7 +123,7 @@ export async function verifyResourceSession( let cleanHost = host; // if the host ends with :port, strip it if (cleanHost.match(/:[0-9]{1,5}$/)) { - let matched = ''+cleanHost.match(/:[0-9]{1,5}$/); + const matched = ''+cleanHost.match(/:[0-9]{1,5}$/); cleanHost = cleanHost.slice(0, -1*matched.length); } diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index f7663f53..77f7d2e0 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -109,7 +109,7 @@ export async function getConfig( ...req.body, endpoint: exitNode[0].endpoint, listenPort: exitNode[0].listenPort - } + }; return proxyToRemote(req, res, next, "hybrid/gerbil/get-config"); } diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 9e2ec8b8..1662e420 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -125,7 +125,7 @@ export async function updateAndGenerateEndpointDestinations( exitNode: ExitNode ) { let currentSiteId: number | undefined; - let destinations: PeerDestination[] = []; + const destinations: PeerDestination[] = []; if (olmId) { logger.debug( diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 536cf9c9..11ca8b5e 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -122,7 +122,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { .where(eq(clientSites.clientId, client.clientId)); // Prepare an array to store site configurations - let siteConfigurations = []; + const siteConfigurations = []; logger.debug( `Found ${sitesData.length} sites for client ${client.clientId}` ); diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 452c7228..f9a67432 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -49,7 +49,7 @@ export async function traefikConfigProvider( // Get the current exit node name from config await getCurrentExitNodeId(); - let traefikConfig = await getTraefikConfig( + const traefikConfig = await getTraefikConfig( currentExitNodeId, config.getRawConfig().traefik.site_types ); diff --git a/server/setup/scriptsPg/1.6.0.ts b/server/setup/scriptsPg/1.6.0.ts index 4e23fe4d..72d79e6c 100644 --- a/server/setup/scriptsPg/1.6.0.ts +++ b/server/setup/scriptsPg/1.6.0.ts @@ -37,7 +37,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); if (rawConfig.server?.trust_proxy) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta10.ts b/server/setup/scriptsSqlite/1.0.0-beta10.ts index cf988f04..96c96f49 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta10.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta10.ts @@ -24,7 +24,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); delete rawConfig.server.secure_cookies; diff --git a/server/setup/scriptsSqlite/1.0.0-beta12.ts b/server/setup/scriptsSqlite/1.0.0-beta12.ts index 2fbc00b6..b3b87b3c 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta12.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta12.ts @@ -26,7 +26,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); if (!rawConfig.flags) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta15.ts b/server/setup/scriptsSqlite/1.0.0-beta15.ts index ef82d029..bbd9fb6c 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta15.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta15.ts @@ -31,7 +31,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); const baseDomain = rawConfig.app.base_domain; diff --git a/server/setup/scriptsSqlite/1.0.0-beta2.ts b/server/setup/scriptsSqlite/1.0.0-beta2.ts index b2ad8386..2418085a 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta2.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta2.ts @@ -23,7 +23,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); // Validate the structure diff --git a/server/setup/scriptsSqlite/1.0.0-beta3.ts b/server/setup/scriptsSqlite/1.0.0-beta3.ts index 36fab908..83fc6978 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta3.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta3.ts @@ -23,7 +23,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); // Validate the structure diff --git a/server/setup/scriptsSqlite/1.0.0-beta5.ts b/server/setup/scriptsSqlite/1.0.0-beta5.ts index 44412ad0..3e80ad67 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta5.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta5.ts @@ -26,7 +26,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); // Validate the structure diff --git a/server/setup/scriptsSqlite/1.0.0-beta6.ts b/server/setup/scriptsSqlite/1.0.0-beta6.ts index ba927b35..5266010a 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta6.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta6.ts @@ -24,7 +24,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); // Validate the structure diff --git a/server/setup/scriptsSqlite/1.0.0-beta9.ts b/server/setup/scriptsSqlite/1.0.0-beta9.ts index 889f19a0..c5ede2f7 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta9.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta9.ts @@ -59,7 +59,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); rawConfig.server.resource_session_request_param = diff --git a/server/setup/scriptsSqlite/1.2.0.ts b/server/setup/scriptsSqlite/1.2.0.ts index c1c0b0b5..c6874766 100644 --- a/server/setup/scriptsSqlite/1.2.0.ts +++ b/server/setup/scriptsSqlite/1.2.0.ts @@ -44,7 +44,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); if (!rawConfig.flags) { diff --git a/server/setup/scriptsSqlite/1.3.0.ts b/server/setup/scriptsSqlite/1.3.0.ts index 820ce0ad..a1f8943a 100644 --- a/server/setup/scriptsSqlite/1.3.0.ts +++ b/server/setup/scriptsSqlite/1.3.0.ts @@ -177,7 +177,7 @@ export default async function migration() { } const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); if (!rawConfig.server.secret) { diff --git a/server/setup/scriptsSqlite/1.5.0.ts b/server/setup/scriptsSqlite/1.5.0.ts index 30dd98e7..f417b46d 100644 --- a/server/setup/scriptsSqlite/1.5.0.ts +++ b/server/setup/scriptsSqlite/1.5.0.ts @@ -45,7 +45,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); if (rawConfig.cors?.headers) { diff --git a/server/setup/scriptsSqlite/1.6.0.ts b/server/setup/scriptsSqlite/1.6.0.ts index 35915b7d..f38f0574 100644 --- a/server/setup/scriptsSqlite/1.6.0.ts +++ b/server/setup/scriptsSqlite/1.6.0.ts @@ -46,7 +46,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - let rawConfig: any; + const rawConfig: any; rawConfig = yaml.load(fileContents); if (rawConfig.server?.trust_proxy) { From 8bcb2b3b0f270997aaa8f82660a9d1a24ba75400 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 23 Aug 2025 15:30:03 -0700 Subject: [PATCH 206/219] Fix type error --- cli/commands/setAdminCredentials.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/commands/setAdminCredentials.ts b/cli/commands/setAdminCredentials.ts index 84a6c795..91a6bcf7 100644 --- a/cli/commands/setAdminCredentials.ts +++ b/cli/commands/setAdminCredentials.ts @@ -32,7 +32,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = { }, handler: async (argv: { email: string; password: string }) => { try { - const { email, password } = argv; + const { password } = argv; + let { email } = argv; email = email.trim().toLowerCase(); const parsed = passwordSchema.safeParse(password); From 5d34bd82c012cb09cbb8a96afbde1670903d53d3 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 23 Aug 2025 15:36:19 -0700 Subject: [PATCH 207/219] Adjust const one more time --- server/setup/scriptsPg/1.6.0.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta10.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta12.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta15.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta2.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta3.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta5.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta6.ts | 3 +-- server/setup/scriptsSqlite/1.0.0-beta9.ts | 3 +-- server/setup/scriptsSqlite/1.2.0.ts | 3 +-- server/setup/scriptsSqlite/1.3.0.ts | 3 +-- server/setup/scriptsSqlite/1.5.0.ts | 3 +-- server/setup/scriptsSqlite/1.6.0.ts | 3 +-- 13 files changed, 13 insertions(+), 26 deletions(-) diff --git a/server/setup/scriptsPg/1.6.0.ts b/server/setup/scriptsPg/1.6.0.ts index 72d79e6c..30c9c269 100644 --- a/server/setup/scriptsPg/1.6.0.ts +++ b/server/setup/scriptsPg/1.6.0.ts @@ -37,8 +37,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; if (rawConfig.server?.trust_proxy) { rawConfig.server.trust_proxy = 1; diff --git a/server/setup/scriptsSqlite/1.0.0-beta10.ts b/server/setup/scriptsSqlite/1.0.0-beta10.ts index 96c96f49..400cbc31 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta10.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta10.ts @@ -24,8 +24,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; delete rawConfig.server.secure_cookies; diff --git a/server/setup/scriptsSqlite/1.0.0-beta12.ts b/server/setup/scriptsSqlite/1.0.0-beta12.ts index b3b87b3c..8c96e663 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta12.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta12.ts @@ -26,8 +26,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; if (!rawConfig.flags) { rawConfig.flags = {}; diff --git a/server/setup/scriptsSqlite/1.0.0-beta15.ts b/server/setup/scriptsSqlite/1.0.0-beta15.ts index bbd9fb6c..cf39fd8a 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta15.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta15.ts @@ -31,8 +31,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; const baseDomain = rawConfig.app.base_domain; const certResolver = rawConfig.traefik.cert_resolver; diff --git a/server/setup/scriptsSqlite/1.0.0-beta2.ts b/server/setup/scriptsSqlite/1.0.0-beta2.ts index 2418085a..1241e9c5 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta2.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta2.ts @@ -23,8 +23,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; // Validate the structure if (!rawConfig.app || !rawConfig.app.base_url) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta3.ts b/server/setup/scriptsSqlite/1.0.0-beta3.ts index 83fc6978..fccfeb88 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta3.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta3.ts @@ -23,8 +23,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; // Validate the structure if (!rawConfig.gerbil) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta5.ts b/server/setup/scriptsSqlite/1.0.0-beta5.ts index 3e80ad67..1c49503c 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta5.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta5.ts @@ -26,8 +26,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; // Validate the structure if (!rawConfig.server) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta6.ts b/server/setup/scriptsSqlite/1.0.0-beta6.ts index 5266010a..89129678 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta6.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta6.ts @@ -24,8 +24,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; // Validate the structure if (!rawConfig.server) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta9.ts b/server/setup/scriptsSqlite/1.0.0-beta9.ts index c5ede2f7..350293dc 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta9.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta9.ts @@ -59,8 +59,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; rawConfig.server.resource_session_request_param = "p_session_request"; diff --git a/server/setup/scriptsSqlite/1.2.0.ts b/server/setup/scriptsSqlite/1.2.0.ts index c6874766..d6008407 100644 --- a/server/setup/scriptsSqlite/1.2.0.ts +++ b/server/setup/scriptsSqlite/1.2.0.ts @@ -44,8 +44,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; if (!rawConfig.flags) { rawConfig.flags = {}; diff --git a/server/setup/scriptsSqlite/1.3.0.ts b/server/setup/scriptsSqlite/1.3.0.ts index a1f8943a..a084d59f 100644 --- a/server/setup/scriptsSqlite/1.3.0.ts +++ b/server/setup/scriptsSqlite/1.3.0.ts @@ -177,8 +177,7 @@ export default async function migration() { } const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; if (!rawConfig.server.secret) { rawConfig.server.secret = generateIdFromEntropySize(32); diff --git a/server/setup/scriptsSqlite/1.5.0.ts b/server/setup/scriptsSqlite/1.5.0.ts index f417b46d..46e9ccca 100644 --- a/server/setup/scriptsSqlite/1.5.0.ts +++ b/server/setup/scriptsSqlite/1.5.0.ts @@ -45,8 +45,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; if (rawConfig.cors?.headers) { const headers = JSON.parse( diff --git a/server/setup/scriptsSqlite/1.6.0.ts b/server/setup/scriptsSqlite/1.6.0.ts index f38f0574..adab2697 100644 --- a/server/setup/scriptsSqlite/1.6.0.ts +++ b/server/setup/scriptsSqlite/1.6.0.ts @@ -46,8 +46,7 @@ export default async function migration() { // Read and parse the YAML file const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig: any; - rawConfig = yaml.load(fileContents); + const rawConfig = yaml.load(fileContents) as any; if (rawConfig.server?.trust_proxy) { rawConfig.server.trust_proxy = 1; From ebed9f7a683e55728a702018cfebe338cf29be76 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:50:52 -0700 Subject: [PATCH 208/219] New translations en-us.json (French) --- messages/fr-FR.json | 246 ++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 0eeb7b85..e2a6fa4a 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.", "siteWg": "WireGuard basique", "siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", "siteLocalDescription": "Ressources locales seulement. Pas de tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", "siteSeeAll": "Voir tous les sites", "siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site", "siteNewtCredentials": "Identifiants Newt", @@ -168,7 +168,7 @@ "siteSelect": "Sélectionner un site", "siteSearch": "Chercher un site", "siteNotFound": "Aucun site trouvé.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Ce site fournira la connectivité à la cible.", "resourceType": "Type de ressource", "resourceTypeDescription": "Déterminer comment vous voulez accéder à votre ressource", "resourceHTTPSSettings": "Paramètres HTTPS", @@ -199,7 +199,7 @@ "general": "Généraux", "generalSettings": "Paramètres généraux", "proxy": "Proxy", - "internal": "Internal", + "internal": "Interne", "rules": "Règles", "resourceSettingDescription": "Configurer les paramètres de votre ressource", "resourceSetting": "Réglages {resourceName}", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.", "targetTlsSubmit": "Enregistrer les paramètres", "targets": "Configuration des cibles", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Configurez les cibles pour router le trafic vers vos services.", "targetStickySessions": "Activer les sessions persistantes", "targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.", "methodSelect": "Sélectionner la méthode", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres", "pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres", "passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Exigences relatives au mot de passe :", + "passwordRequirementLength": "Au moins 8 caractères", + "passwordRequirementUppercase": "Au moins une lettre majuscule", + "passwordRequirementLowercase": "Au moins une lettre minuscule", + "passwordRequirementNumber": "Au moins un chiffre", + "passwordRequirementSpecial": "Au moins un caractère spécial", + "passwordRequirementsMet": "✓ Le mot de passe répond à toutes les exigences", + "passwordStrength": "Solidité du mot de passe", + "passwordStrengthWeak": "Faible", + "passwordStrengthMedium": "Moyen", + "passwordStrengthStrong": "Fort", + "passwordRequirements": "Exigences :", + "passwordRequirementLengthText": "8+ caractères", + "passwordRequirementUppercaseText": "Lettre majuscule (A-Z)", + "passwordRequirementLowercaseText": "Lettre minuscule (a-z)", + "passwordRequirementNumberText": "Nombre (0-9)", + "passwordRequirementSpecialText": "Caractère spécial (!@#$%...)", + "passwordsDoNotMatch": "Les mots de passe ne correspondent pas", "otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère", "otpEmailSent": "OTP envoyé", "otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail", @@ -973,7 +973,7 @@ "logoutError": "Erreur lors de la déconnexion", "signingAs": "Connecté en tant que", "serverAdmin": "Admin Serveur", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Gestion autonome", "otpEnable": "Activer l'authentification à deux facteurs", "otpDisable": "Désactiver l'authentification à deux facteurs", "logout": "Déconnexion", @@ -989,9 +989,9 @@ "actionDeleteSite": "Supprimer un site", "actionGetSite": "Obtenir un site", "actionListSites": "Lister les sites", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Jeton de configuration", + "setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.", + "setupTokenRequired": "Le jeton de configuration est requis.", "actionUpdateSite": "Mettre à jour un site", "actionListSiteRoles": "Lister les rôles autorisés du site", "actionCreateResource": "Créer une ressource", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version d'Olm.", "remoteSubnets": "Sous-réseaux distants", "enterCidrRange": "Entrez la plage CIDR", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Ajoutez des plages CIDR accessibles à distance depuis ce site à l'aide de clients. Utilisez le format comme 10.0.0.0/24. Cela s'applique UNIQUEMENT à la connectivité des clients VPN.", "resourceEnableProxy": "Activer le proxy public", "resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.", "externalProxyEnabled": "Proxy externe activé", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", + "addNewTarget": "Ajouter une nouvelle cible", + "targetsList": "Liste des cibles", + "targetErrorDuplicateTargetFound": "Cible en double trouvée", + "httpMethod": "Méthode HTTP", + "selectHttpMethod": "Sélectionnez la méthode HTTP", + "domainPickerSubdomainLabel": "Sous-domaine", + "domainPickerBaseDomainLabel": "Domaine de base", + "domainPickerSearchDomains": "Rechercher des domaines...", + "domainPickerNoDomainsFound": "Aucun domaine trouvé", + "domainPickerLoadingDomains": "Chargement des domaines...", + "domainPickerSelectBaseDomain": "Sélectionnez le domaine de base...", + "domainPickerNotAvailableForCname": "Non disponible pour les domaines CNAME", + "domainPickerEnterSubdomainOrLeaveBlank": "Entrez un sous-domaine ou laissez vide pour utiliser le domaine de base.", + "domainPickerEnterSubdomainToSearch": "Entrez un sous-domaine pour rechercher et sélectionner parmi les domaines gratuits disponibles.", + "domainPickerFreeDomains": "Domaines gratuits", + "domainPickerSearchForAvailableDomains": "Rechercher des domaines disponibles", + "resourceDomain": "Domaine", + "resourceEditDomain": "Modifier le domaine", + "siteName": "Nom du site", "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableProxyResources": "Ressources proxy", + "resourcesTableClientResources": "Ressources client", + "resourcesTableNoProxyResourcesFound": "Aucune ressource proxy trouvée.", + "resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.", "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableTheseResourcesForUseWith": "Ces ressources sont à utiliser avec", "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", + "resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.", + "editInternalResourceDialogEditClientResource": "Modifier la ressource client", + "editInternalResourceDialogUpdateResourceProperties": "Mettez à jour les propriétés de la ressource et la configuration de la cible pour {resourceName}.", + "editInternalResourceDialogResourceProperties": "Propriétés de la ressource", + "editInternalResourceDialogName": "Nom", + "editInternalResourceDialogProtocol": "Protocole", + "editInternalResourceDialogSitePort": "Port du site", + "editInternalResourceDialogTargetConfiguration": "Configuration de la cible", + "editInternalResourceDialogDestinationIP": "IP de destination", + "editInternalResourceDialogDestinationPort": "Port de destination", + "editInternalResourceDialogCancel": "Abandonner", + "editInternalResourceDialogSaveResource": "Enregistrer la ressource", + "editInternalResourceDialogSuccess": "Succès", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Ressource interne mise à jour avec succès", + "editInternalResourceDialogError": "Erreur", + "editInternalResourceDialogFailedToUpdateInternalResource": "Échec de la mise à jour de la ressource interne", + "editInternalResourceDialogNameRequired": "Le nom est requis", + "editInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères", + "editInternalResourceDialogProxyPortMin": "Le port proxy doit être d'au moins 1", + "editInternalResourceDialogProxyPortMax": "Le port proxy doit être inférieur à 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Format d'adresse IP invalide", + "editInternalResourceDialogDestinationPortMin": "Le port de destination doit être d'au moins 1", + "editInternalResourceDialogDestinationPortMax": "Le port de destination doit être inférieur à 65536", + "createInternalResourceDialogNoSitesAvailable": "Aucun site disponible", + "createInternalResourceDialogNoSitesAvailableDescription": "Vous devez avoir au moins un site Newt avec un sous-réseau configuré pour créer des ressources internes.", + "createInternalResourceDialogClose": "Fermer", + "createInternalResourceDialogCreateClientResource": "Créer une ressource client", + "createInternalResourceDialogCreateClientResourceDescription": "Créez une ressource accessible aux clients connectés au site sélectionné.", + "createInternalResourceDialogResourceProperties": "Propriétés de la ressource", + "createInternalResourceDialogName": "Nom", "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogSelectSite": "Sélectionner un site...", + "createInternalResourceDialogSearchSites": "Rechercher des sites...", + "createInternalResourceDialogNoSitesFound": "Aucun site trouvé.", + "createInternalResourceDialogProtocol": "Protocole", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogSitePort": "Port du site", + "createInternalResourceDialogSitePortDescription": "Utilisez ce port pour accéder à la ressource sur le site lors de la connexion avec un client.", + "createInternalResourceDialogTargetConfiguration": "Configuration de la cible", + "createInternalResourceDialogDestinationIP": "IP de destination", + "createInternalResourceDialogDestinationIPDescription": "L'adresse IP de la ressource sur le réseau du site.", + "createInternalResourceDialogDestinationPort": "Port de destination", + "createInternalResourceDialogDestinationPortDescription": "Le port sur l'IP de destination où la ressource est accessible.", + "createInternalResourceDialogCancel": "Abandonner", + "createInternalResourceDialogCreateResource": "Créer une ressource", + "createInternalResourceDialogSuccess": "Succès", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Ressource interne créée avec succès", + "createInternalResourceDialogError": "Erreur", + "createInternalResourceDialogFailedToCreateInternalResource": "Échec de la création de la ressource interne", + "createInternalResourceDialogNameRequired": "Le nom est requis", + "createInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères", + "createInternalResourceDialogPleaseSelectSite": "Veuillez sélectionner un site", + "createInternalResourceDialogProxyPortMin": "Le port proxy doit être d'au moins 1", + "createInternalResourceDialogProxyPortMax": "Le port proxy doit être inférieur à 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Format d'adresse IP invalide", + "createInternalResourceDialogDestinationPortMin": "Le port de destination doit être d'au moins 1", + "createInternalResourceDialogDestinationPortMax": "Le port de destination doit être inférieur à 65536", "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "siteAcceptClientConnections": "Accepter les connexions client", + "siteAcceptClientConnectionsDescription": "Permet à d'autres appareils de se connecter via cette instance de Newt en tant que passerelle utilisant des clients.", + "siteAddress": "Adresse du site", + "siteAddressDescription": "Spécifiez l'adresse IP de l'hôte pour que les clients puissent s'y connecter. C'est l'adresse interne du site dans le réseau Pangolin pour que les clients puissent s'adresser. Doit être dans le sous-réseau de l'organisation.", + "autoLoginExternalIdp": "Connexion automatique avec IDP externe", + "autoLoginExternalIdpDescription": "Rediriger immédiatement l'utilisateur vers l'IDP externe pour l'authentification.", + "selectIdp": "Sélectionner l'IDP", + "selectIdpPlaceholder": "Choisissez un IDP...", + "selectIdpRequired": "Veuillez sélectionner un IDP lorsque la connexion automatique est activée.", + "autoLoginTitle": "Redirection", + "autoLoginDescription": "Redirection vers le fournisseur d'identité externe pour l'authentification.", + "autoLoginProcessing": "Préparation de l'authentification...", + "autoLoginRedirecting": "Redirection vers la connexion...", + "autoLoginError": "Erreur de connexion automatique", + "autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.", + "autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification." } From 9bd7002917da85c3a380ce1a18bdd13182158d5f Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:50:53 -0700 Subject: [PATCH 209/219] New translations en-us.json (Spanish) --- messages/es-ES.json | 252 ++++++++++++++++++++++---------------------- 1 file changed, 126 insertions(+), 126 deletions(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index 22981175..f4d03d69 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en tu red. Sin configuración adicional.", "siteWg": "Wirex Guardia Básica", "siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Utilice cualquier cliente de WireGuard para establecer un túnel. Se requiere configuración manual de NAT. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS", "siteLocalDescription": "Solo recursos locales. Sin túneles.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Solo recursos locales. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS", "siteSeeAll": "Ver todos los sitios", "siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio", "siteNewtCredentials": "Credenciales nuevas", @@ -168,7 +168,7 @@ "siteSelect": "Seleccionar sitio", "siteSearch": "Buscar sitio", "siteNotFound": "Sitio no encontrado.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Este sitio proporcionará conectividad al objetivo.", "resourceType": "Tipo de recurso", "resourceTypeDescription": "Determina cómo quieres acceder a tu recurso", "resourceHTTPSSettings": "Configuración HTTPS", @@ -199,7 +199,7 @@ "general": "General", "generalSettings": "Configuración General", "proxy": "Proxy", - "internal": "Internal", + "internal": "Interno", "rules": "Reglas", "resourceSettingDescription": "Configure la configuración de su recurso", "resourceSetting": "Ajustes {resourceName}", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.", "targetTlsSubmit": "Guardar ajustes", "targets": "Configuración de objetivos", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Configurar objetivos para enrutar tráfico a sus servicios", "targetStickySessions": "Activar Sesiones Pegadas", "targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.", "methodSelect": "Seleccionar método", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos", "pincodeRequirementsChars": "El PIN sólo debe contener números", "passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Requisitos de la contraseña:", + "passwordRequirementLength": "Al menos 8 caracteres de largo", + "passwordRequirementUppercase": "Al menos una letra mayúscula", + "passwordRequirementLowercase": "Al menos una letra minúscula", + "passwordRequirementNumber": "Al menos un número", + "passwordRequirementSpecial": "Al menos un carácter especial", + "passwordRequirementsMet": "✓ La contraseña cumple con todos los requisitos", + "passwordStrength": "Seguridad de la contraseña", + "passwordStrengthWeak": "Débil", + "passwordStrengthMedium": "Media", + "passwordStrengthStrong": "Fuerte", + "passwordRequirements": "Requisitos:", + "passwordRequirementLengthText": "8+ caracteres", + "passwordRequirementUppercaseText": "Letra mayúscula (A-Z)", + "passwordRequirementLowercaseText": "Letra minúscula (a-z)", + "passwordRequirementNumberText": "Número (0-9)", + "passwordRequirementSpecialText": "Caracter especial (!@#$%...)", + "passwordsDoNotMatch": "Las contraseñas no coinciden", "otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter", "otpEmailSent": "OTP enviado", "otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico", @@ -973,7 +973,7 @@ "logoutError": "Error al cerrar sesión", "signingAs": "Conectado como", "serverAdmin": "Admin Servidor", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Autogestionado", "otpEnable": "Activar doble factor", "otpDisable": "Desactivar doble factor", "logout": "Cerrar sesión", @@ -989,9 +989,9 @@ "actionDeleteSite": "Eliminar sitio", "actionGetSite": "Obtener sitio", "actionListSites": "Listar sitios", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Configuración de token", + "setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.", + "setupTokenRequired": "Se requiere el token de configuración", "actionUpdateSite": "Actualizar sitio", "actionListSiteRoles": "Lista de roles permitidos del sitio", "actionCreateResource": "Crear Recurso", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.", "remoteSubnets": "Subredes remotas", "enterCidrRange": "Ingresa el rango CIDR", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Agregue rangos CIDR que se puedan acceder desde este sitio de forma remota usando clientes. Utilice el formato como 10.0.0.0/24. Esto SOLO se aplica a la conectividad del cliente VPN.", "resourceEnableProxy": "Habilitar proxy público", "resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.", "externalProxyEnabled": "Proxy externo habilitado", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "addNewTarget": "Agregar nuevo destino", + "targetsList": "Lista de destinos", + "targetErrorDuplicateTargetFound": "Se encontró un destino duplicado", + "httpMethod": "Método HTTP", + "selectHttpMethod": "Seleccionar método HTTP", + "domainPickerSubdomainLabel": "Subdominio", + "domainPickerBaseDomainLabel": "Dominio base", + "domainPickerSearchDomains": "Buscar dominios...", + "domainPickerNoDomainsFound": "No se encontraron dominios", + "domainPickerLoadingDomains": "Cargando dominios...", + "domainPickerSelectBaseDomain": "Seleccionar dominio base...", + "domainPickerNotAvailableForCname": "No disponible para dominios CNAME", + "domainPickerEnterSubdomainOrLeaveBlank": "Ingrese subdominio o deje en blanco para usar dominio base.", + "domainPickerEnterSubdomainToSearch": "Ingrese un subdominio para buscar y seleccionar entre dominios gratuitos disponibles.", + "domainPickerFreeDomains": "Dominios gratuitos", + "domainPickerSearchForAvailableDomains": "Buscar dominios disponibles", + "resourceDomain": "Dominio", + "resourceEditDomain": "Editar dominio", + "siteName": "Nombre del sitio", + "proxyPort": "Puerto", + "resourcesTableProxyResources": "Recursos de proxy", + "resourcesTableClientResources": "Recursos del cliente", + "resourcesTableNoProxyResourcesFound": "No se encontraron recursos de proxy.", + "resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.", + "resourcesTableDestination": "Destino", + "resourcesTableTheseResourcesForUseWith": "Estos recursos son para uso con", + "resourcesTableClients": "Clientes", + "resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.", + "editInternalResourceDialogEditClientResource": "Editar recurso del cliente", + "editInternalResourceDialogUpdateResourceProperties": "Actualizar las propiedades del recurso y la configuración del objetivo para {resourceName}.", + "editInternalResourceDialogResourceProperties": "Propiedades del recurso", + "editInternalResourceDialogName": "Nombre", + "editInternalResourceDialogProtocol": "Protocolo", + "editInternalResourceDialogSitePort": "Puerto del sitio", + "editInternalResourceDialogTargetConfiguration": "Configuración de objetivos", + "editInternalResourceDialogDestinationIP": "IP de destino", + "editInternalResourceDialogDestinationPort": "Puerto de destino", + "editInternalResourceDialogCancel": "Cancelar", + "editInternalResourceDialogSaveResource": "Guardar recurso", + "editInternalResourceDialogSuccess": "Éxito", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno actualizado con éxito", "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogFailedToUpdateInternalResource": "Error al actualizar el recurso interno", + "editInternalResourceDialogNameRequired": "El nombre es requerido", + "editInternalResourceDialogNameMaxLength": "El nombre no debe tener más de 255 caracteres", + "editInternalResourceDialogProxyPortMin": "El puerto del proxy debe ser al menos 1", + "editInternalResourceDialogProxyPortMax": "El puerto del proxy debe ser menor de 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Formato de dirección IP inválido", + "editInternalResourceDialogDestinationPortMin": "El puerto de destino debe ser al menos 1", + "editInternalResourceDialogDestinationPortMax": "El puerto de destino debe ser menor de 65536", + "createInternalResourceDialogNoSitesAvailable": "No hay sitios disponibles", + "createInternalResourceDialogNoSitesAvailableDescription": "Necesita tener al menos un sitio de Newt con una subred configurada para crear recursos internos.", + "createInternalResourceDialogClose": "Cerrar", + "createInternalResourceDialogCreateClientResource": "Crear recurso del cliente", + "createInternalResourceDialogCreateClientResourceDescription": "Crear un nuevo recurso que será accesible para los clientes conectados al sitio seleccionado.", + "createInternalResourceDialogResourceProperties": "Propiedades del recurso", + "createInternalResourceDialogName": "Nombre", + "createInternalResourceDialogSite": "Sitio", + "createInternalResourceDialogSelectSite": "Seleccionar sitio...", + "createInternalResourceDialogSearchSites": "Buscar sitios...", + "createInternalResourceDialogNoSitesFound": "Sitios no encontrados.", + "createInternalResourceDialogProtocol": "Protocolo", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogSitePort": "Puerto del sitio", + "createInternalResourceDialogSitePortDescription": "Use este puerto para acceder al recurso en el sitio cuando se conecta con un cliente.", + "createInternalResourceDialogTargetConfiguration": "Configuración de objetivos", + "createInternalResourceDialogDestinationIP": "IP de destino", + "createInternalResourceDialogDestinationIPDescription": "La dirección IP del recurso en la red del sitio.", + "createInternalResourceDialogDestinationPort": "Puerto de destino", + "createInternalResourceDialogDestinationPortDescription": "El puerto en la IP de destino donde el recurso es accesible.", + "createInternalResourceDialogCancel": "Cancelar", + "createInternalResourceDialogCreateResource": "Crear recurso", + "createInternalResourceDialogSuccess": "Éxito", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno creado con éxito", "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogFailedToCreateInternalResource": "Error al crear recurso interno", + "createInternalResourceDialogNameRequired": "El nombre es requerido", + "createInternalResourceDialogNameMaxLength": "El nombre debe ser menor de 255 caracteres", + "createInternalResourceDialogPleaseSelectSite": "Por favor seleccione un sitio", + "createInternalResourceDialogProxyPortMin": "El puerto del proxy debe ser al menos 1", + "createInternalResourceDialogProxyPortMax": "El puerto del proxy debe ser menor de 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Formato de dirección IP inválido", + "createInternalResourceDialogDestinationPortMin": "El puerto de destino debe ser al menos 1", + "createInternalResourceDialogDestinationPortMax": "El puerto de destino debe ser menor de 65536", + "siteConfiguration": "Configuración", + "siteAcceptClientConnections": "Aceptar conexiones de clientes", + "siteAcceptClientConnectionsDescription": "Permitir que otros dispositivos se conecten a través de esta instancia Newt como una puerta de enlace utilizando clientes.", + "siteAddress": "Dirección del sitio", + "siteAddressDescription": "Especifique la dirección IP del host que los clientes deben usar para conectarse. Esta es la dirección interna del sitio en la red de Pangolín para que los clientes dirijan. Debe estar dentro de la subred de la organización.", + "autoLoginExternalIdp": "Inicio de sesión automático con IDP externo", + "autoLoginExternalIdpDescription": "Redirigir inmediatamente al usuario al IDP externo para autenticación.", + "selectIdp": "Seleccionar IDP", + "selectIdpPlaceholder": "Elegir un IDP...", + "selectIdpRequired": "Por favor seleccione un IDP cuando el inicio de sesión automático esté habilitado.", + "autoLoginTitle": "Redirigiendo", + "autoLoginDescription": "Te estamos redirigiendo al proveedor de identidad externo para autenticación.", + "autoLoginProcessing": "Preparando autenticación...", + "autoLoginRedirecting": "Redirigiendo al inicio de sesión...", + "autoLoginError": "Error de inicio de sesión automático", + "autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.", + "autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación." } From 34d3ca9c51569e9e60520273769b36e6f6803926 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:50:55 -0700 Subject: [PATCH 210/219] New translations en-us.json (German) --- messages/de-DE.json | 246 ++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index 68123500..c31b691f 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.", "siteWg": "Einfacher WireGuard Tunnel", "siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Verwenden Sie jeden WireGuard-Client, um einen Tunnel zu erstellen. Manuelles NAT-Setup erforderlich. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN", "siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Nur lokale Ressourcen. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN", "siteSeeAll": "Alle Standorte anzeigen", "siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest", "siteNewtCredentials": "Neue Newt Zugangsdaten", @@ -168,7 +168,7 @@ "siteSelect": "Standort auswählen", "siteSearch": "Standorte durchsuchen", "siteNotFound": "Keinen Standort gefunden.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Dieser Standort wird die Verbindung zum Ziel herstellen.", "resourceType": "Ressourcentyp", "resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten", "resourceHTTPSSettings": "HTTPS-Einstellungen", @@ -199,7 +199,7 @@ "general": "Allgemein", "generalSettings": "Allgemeine Einstellungen", "proxy": "Proxy", - "internal": "Internal", + "internal": "Intern", "rules": "Regeln", "resourceSettingDescription": "Konfigurieren Sie die Einstellungen Ihrer Ressource", "resourceSetting": "{resourceName} Einstellungen", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.", "targetTlsSubmit": "Einstellungen speichern", "targets": "Ziel-Konfiguration", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Richten Sie Ziele ein, um Datenverkehr zu Ihren Backend-Diensten zu leiten", "targetStickySessions": "Sticky Sessions aktivieren", "targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.", "methodSelect": "Methode auswählen", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein", "pincodeRequirementsChars": "PIN darf nur Zahlen enthalten", "passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Passwortanforderungen:", + "passwordRequirementLength": "Mindestens 8 Zeichen lang", + "passwordRequirementUppercase": "Mindestens ein Großbuchstabe", + "passwordRequirementLowercase": "Mindestens ein Kleinbuchstabe", + "passwordRequirementNumber": "Mindestens eine Zahl", + "passwordRequirementSpecial": "Mindestens ein Sonderzeichen", + "passwordRequirementsMet": "✓ Passwort erfüllt alle Anforderungen", + "passwordStrength": "Passwortstärke", + "passwordStrengthWeak": "Schwach", + "passwordStrengthMedium": "Mittel", + "passwordStrengthStrong": "Stark", + "passwordRequirements": "Anforderungen:", + "passwordRequirementLengthText": "8+ Zeichen", + "passwordRequirementUppercaseText": "Großbuchstabe (A-Z)", + "passwordRequirementLowercaseText": "Kleinbuchstabe (a-z)", + "passwordRequirementNumberText": "Zahl (0-9)", + "passwordRequirementSpecialText": "Sonderzeichen (!@#$%...)", + "passwordsDoNotMatch": "Passwörter stimmen nicht überein", "otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein", "otpEmailSent": "OTP gesendet", "otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet", @@ -973,7 +973,7 @@ "logoutError": "Fehler beim Abmelden", "signingAs": "Angemeldet als", "serverAdmin": "Server-Administrator", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Verwaltetes Selbsthosted", "otpEnable": "Zwei-Faktor aktivieren", "otpDisable": "Zwei-Faktor deaktivieren", "logout": "Abmelden", @@ -989,9 +989,9 @@ "actionDeleteSite": "Standort löschen", "actionGetSite": "Standort abrufen", "actionListSites": "Standorte auflisten", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Setup-Token", + "setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.", + "setupTokenRequired": "Setup-Token ist erforderlich", "actionUpdateSite": "Standorte aktualisieren", "actionListSiteRoles": "Erlaubte Standort-Rollen auflisten", "actionCreateResource": "Ressource erstellen", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.", "remoteSubnets": "Remote-Subnetze", "enterCidrRange": "Geben Sie den CIDR-Bereich ein", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Fügen Sie CIDR-Bereiche hinzu, die über Clients von dieser Site aus remote zugänglich sind. Verwenden Sie ein Format wie 10.0.0.0/24. Dies gilt NUR für die VPN-Client-Konnektivität.", "resourceEnableProxy": "Öffentlichen Proxy aktivieren", "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", "externalProxyEnabled": "Externer Proxy aktiviert", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", + "addNewTarget": "Neues Ziel hinzufügen", + "targetsList": "Ziel-Liste", + "targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden", + "httpMethod": "HTTP-Methode", + "selectHttpMethod": "HTTP-Methode auswählen", "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", + "domainPickerBaseDomainLabel": "Basisdomäne", + "domainPickerSearchDomains": "Domains suchen...", + "domainPickerNoDomainsFound": "Keine Domains gefunden", + "domainPickerLoadingDomains": "Domains werden geladen...", + "domainPickerSelectBaseDomain": "Basisdomäne auswählen...", + "domainPickerNotAvailableForCname": "Für CNAME-Domains nicht verfügbar", + "domainPickerEnterSubdomainOrLeaveBlank": "Geben Sie eine Subdomain ein oder lassen Sie das Feld leer, um die Basisdomäne zu verwenden.", + "domainPickerEnterSubdomainToSearch": "Geben Sie eine Subdomain ein, um verfügbare freie Domains zu suchen und auszuwählen.", + "domainPickerFreeDomains": "Freie Domains", + "domainPickerSearchForAvailableDomains": "Verfügbare Domains suchen", "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", + "resourceEditDomain": "Domain bearbeiten", + "siteName": "Site-Name", "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", + "resourcesTableProxyResources": "Proxy-Ressourcen", + "resourcesTableClientResources": "Client-Ressourcen", + "resourcesTableNoProxyResourcesFound": "Keine Proxy-Ressourcen gefunden.", + "resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.", + "resourcesTableDestination": "Ziel", + "resourcesTableTheseResourcesForUseWith": "Diese Ressourcen sind zur Verwendung mit", + "resourcesTableClients": "Kunden", + "resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.", + "editInternalResourceDialogEditClientResource": "Client-Ressource bearbeiten", + "editInternalResourceDialogUpdateResourceProperties": "Aktualisieren Sie die Ressourceneigenschaften und die Zielkonfiguration für {resourceName}.", + "editInternalResourceDialogResourceProperties": "Ressourceneigenschaften", "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogProtocol": "Protokoll", + "editInternalResourceDialogSitePort": "Site-Port", + "editInternalResourceDialogTargetConfiguration": "Zielkonfiguration", + "editInternalResourceDialogDestinationIP": "Ziel-IP", + "editInternalResourceDialogDestinationPort": "Ziel-Port", + "editInternalResourceDialogCancel": "Abbrechen", + "editInternalResourceDialogSaveResource": "Ressource speichern", + "editInternalResourceDialogSuccess": "Erfolg", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne Ressource erfolgreich aktualisiert", + "editInternalResourceDialogError": "Fehler", + "editInternalResourceDialogFailedToUpdateInternalResource": "Interne Ressource konnte nicht aktualisiert werden", + "editInternalResourceDialogNameRequired": "Name ist erforderlich", + "editInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein", + "editInternalResourceDialogProxyPortMin": "Proxy-Port muss mindestens 1 sein", + "editInternalResourceDialogProxyPortMax": "Proxy-Port muss kleiner als 65536 sein", + "editInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat", + "editInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein", + "editInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein", + "createInternalResourceDialogNoSitesAvailable": "Keine Sites verfügbar", + "createInternalResourceDialogNoSitesAvailableDescription": "Sie müssen mindestens eine Newt-Site mit einem konfigurierten Subnetz haben, um interne Ressourcen zu erstellen.", + "createInternalResourceDialogClose": "Schließen", + "createInternalResourceDialogCreateClientResource": "Ressource erstellen", + "createInternalResourceDialogCreateClientResourceDescription": "Erstellen Sie eine neue Ressource, die für Clients zugänglich ist, die mit der ausgewählten Site verbunden sind.", + "createInternalResourceDialogResourceProperties": "Ressourceneigenschaften", "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogSite": "Standort", + "createInternalResourceDialogSelectSite": "Standort auswählen...", + "createInternalResourceDialogSearchSites": "Sites durchsuchen...", + "createInternalResourceDialogNoSitesFound": "Keine Standorte gefunden.", + "createInternalResourceDialogProtocol": "Protokoll", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Site-Port", + "createInternalResourceDialogSitePortDescription": "Verwenden Sie diesen Port, um bei Verbindung mit einem Client auf die Ressource an der Site zuzugreifen.", + "createInternalResourceDialogTargetConfiguration": "Zielkonfiguration", + "createInternalResourceDialogDestinationIP": "Ziel-IP", + "createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse der Ressource im Netzwerkstandort der Site.", + "createInternalResourceDialogDestinationPort": "Ziel-Port", + "createInternalResourceDialogDestinationPortDescription": "Der Port auf der Ziel-IP, unter dem die Ressource zugänglich ist.", + "createInternalResourceDialogCancel": "Abbrechen", + "createInternalResourceDialogCreateResource": "Ressource erstellen", + "createInternalResourceDialogSuccess": "Erfolg", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne Ressource erfolgreich erstellt", + "createInternalResourceDialogError": "Fehler", + "createInternalResourceDialogFailedToCreateInternalResource": "Interne Ressource konnte nicht erstellt werden", + "createInternalResourceDialogNameRequired": "Name ist erforderlich", + "createInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein", + "createInternalResourceDialogPleaseSelectSite": "Bitte wählen Sie eine Site aus", + "createInternalResourceDialogProxyPortMin": "Proxy-Port muss mindestens 1 sein", + "createInternalResourceDialogProxyPortMax": "Proxy-Port muss kleiner als 65536 sein", + "createInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat", + "createInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein", + "createInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein", + "siteConfiguration": "Konfiguration", + "siteAcceptClientConnections": "Clientverbindungen akzeptieren", + "siteAcceptClientConnectionsDescription": "Erlauben Sie anderen Geräten, über diese Newt-Instanz mit Clients als Gateway zu verbinden.", + "siteAddress": "Site-Adresse", + "siteAddressDescription": "Geben Sie die IP-Adresse des Hosts an, mit dem sich die Clients verbinden sollen. Dies ist die interne Adresse der Site im Pangolin-Netzwerk, die von Clients angesprochen werden muss. Muss innerhalb des Unternehmens-Subnetzes liegen.", + "autoLoginExternalIdp": "Automatische Anmeldung mit externem IDP", + "autoLoginExternalIdpDescription": "Leiten Sie den Benutzer sofort zur Authentifizierung an den externen IDP weiter.", + "selectIdp": "IDP auswählen", + "selectIdpPlaceholder": "Wählen Sie einen IDP...", + "selectIdpRequired": "Bitte wählen Sie einen IDP aus, wenn automatische Anmeldung aktiviert ist.", + "autoLoginTitle": "Weiterleitung", + "autoLoginDescription": "Sie werden zum externen Identitätsanbieter zur Authentifizierung weitergeleitet.", + "autoLoginProcessing": "Authentifizierung vorbereiten...", + "autoLoginRedirecting": "Weiterleitung zur Anmeldung...", + "autoLoginError": "Fehler bei der automatischen Anmeldung", + "autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.", + "autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL." } From 4229324a5d0a703e3717307eb113556f583c3c03 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:50:57 -0700 Subject: [PATCH 211/219] New translations en-us.json (Italian) --- messages/it-IT.json | 256 ++++++++++++++++++++++---------------------- 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index c21797d0..803e94a9 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint nella rete. Nessuna configurazione aggiuntiva.", "siteWg": "WireGuard Base", "siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI", "siteLocalDescription": "Solo risorse locali. Nessun tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. FUNZIONA SOLO SU NODI AUTO-OSPITATI", "siteSeeAll": "Vedi Tutti I Siti", "siteTunnelDescription": "Determina come vuoi connetterti al tuo sito", "siteNewtCredentials": "Credenziali Newt", @@ -168,7 +168,7 @@ "siteSelect": "Seleziona sito", "siteSearch": "Cerca sito", "siteNotFound": "Nessun sito trovato.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.", "resourceType": "Tipo Di Risorsa", "resourceTypeDescription": "Determina come vuoi accedere alla tua risorsa", "resourceHTTPSSettings": "Impostazioni HTTPS", @@ -199,7 +199,7 @@ "general": "Generale", "generalSettings": "Impostazioni Generali", "proxy": "Proxy", - "internal": "Internal", + "internal": "Interno", "rules": "Regole", "resourceSettingDescription": "Configura le impostazioni sulla tua risorsa", "resourceSetting": "Impostazioni {resourceName}", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.", "targetTlsSubmit": "Salva Impostazioni", "targets": "Configurazione Target", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Configura i target per instradare il traffico ai tuoi servizi backend", "targetStickySessions": "Abilita Sessioni Persistenti", "targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.", "methodSelect": "Seleziona metodo", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre", "pincodeRequirementsChars": "Il PIN deve contenere solo numeri", "passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Requisiti della password:", + "passwordRequirementLength": "Almeno 8 caratteri", + "passwordRequirementUppercase": "Almeno una lettera maiuscola", + "passwordRequirementLowercase": "Almeno una lettera minuscola", + "passwordRequirementNumber": "Almeno un numero", + "passwordRequirementSpecial": "Almeno un carattere speciale", + "passwordRequirementsMet": "✓ La password soddisfa tutti i requisiti", + "passwordStrength": "Forza della password", + "passwordStrengthWeak": "Debole", + "passwordStrengthMedium": "Media", + "passwordStrengthStrong": "Forte", + "passwordRequirements": "Requisiti:", + "passwordRequirementLengthText": "8+ caratteri", + "passwordRequirementUppercaseText": "Lettera maiuscola (A-Z)", + "passwordRequirementLowercaseText": "Lettera minuscola (a-z)", + "passwordRequirementNumberText": "Numero (0-9)", + "passwordRequirementSpecialText": "Carattere speciale (!@#$%...)", + "passwordsDoNotMatch": "Le password non coincidono", "otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere", "otpEmailSent": "OTP Inviato", "otpEmailSentDescription": "Un OTP è stato inviato alla tua email", @@ -973,7 +973,7 @@ "logoutError": "Errore durante il logout", "signingAs": "Accesso come", "serverAdmin": "Amministratore Server", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Gestito Auto-Ospitato", "otpEnable": "Abilita Autenticazione a Due Fattori", "otpDisable": "Disabilita Autenticazione a Due Fattori", "logout": "Disconnetti", @@ -989,9 +989,9 @@ "actionDeleteSite": "Elimina Sito", "actionGetSite": "Ottieni Sito", "actionListSites": "Elenca Siti", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Configura Token", + "setupTokenDescription": "Inserisci il token di configurazione dalla console del server.", + "setupTokenRequired": "Il token di configurazione è richiesto", "actionUpdateSite": "Aggiorna Sito", "actionListSiteRoles": "Elenca Ruoli Sito Consentiti", "actionCreateResource": "Crea Risorsa", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.", "remoteSubnets": "Sottoreti Remote", "enterCidrRange": "Inserisci l'intervallo CIDR", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Aggiungi intervalli CIDR che possono essere accessibili da questo sito in remoto utilizzando i client. Usa il formato come 10.0.0.0/24. Questo si applica SOLO alla connettività del client VPN.", "resourceEnableProxy": "Abilita Proxy Pubblico", "resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.", "externalProxyEnabled": "Proxy Esterno Abilitato", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "addNewTarget": "Aggiungi Nuovo Target", + "targetsList": "Elenco dei Target", + "targetErrorDuplicateTargetFound": "Target duplicato trovato", + "httpMethod": "Metodo HTTP", + "selectHttpMethod": "Seleziona metodo HTTP", + "domainPickerSubdomainLabel": "Sottodominio", + "domainPickerBaseDomainLabel": "Dominio Base", + "domainPickerSearchDomains": "Cerca domini...", + "domainPickerNoDomainsFound": "Nessun dominio trovato", + "domainPickerLoadingDomains": "Caricamento domini...", + "domainPickerSelectBaseDomain": "Seleziona dominio base...", + "domainPickerNotAvailableForCname": "Non disponibile per i domini CNAME", + "domainPickerEnterSubdomainOrLeaveBlank": "Inserisci un sottodominio o lascia vuoto per utilizzare il dominio base.", + "domainPickerEnterSubdomainToSearch": "Inserisci un sottodominio per cercare e selezionare dai domini gratuiti disponibili.", + "domainPickerFreeDomains": "Domini Gratuiti", + "domainPickerSearchForAvailableDomains": "Cerca domini disponibili", + "resourceDomain": "Dominio", + "resourceEditDomain": "Modifica Dominio", + "siteName": "Nome del Sito", + "proxyPort": "Porta", + "resourcesTableProxyResources": "Risorse Proxy", + "resourcesTableClientResources": "Risorse Client", + "resourcesTableNoProxyResourcesFound": "Nessuna risorsa proxy trovata.", + "resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.", + "resourcesTableDestination": "Destinazione", + "resourcesTableTheseResourcesForUseWith": "Queste risorse sono per uso con", + "resourcesTableClients": "Client", + "resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.", + "editInternalResourceDialogEditClientResource": "Modifica Risorsa Client", + "editInternalResourceDialogUpdateResourceProperties": "Aggiorna le proprietà della risorsa e la configurazione del target per {resourceName}.", + "editInternalResourceDialogResourceProperties": "Proprietà della Risorsa", + "editInternalResourceDialogName": "Nome", + "editInternalResourceDialogProtocol": "Protocollo", + "editInternalResourceDialogSitePort": "Porta del Sito", + "editInternalResourceDialogTargetConfiguration": "Configurazione Target", + "editInternalResourceDialogDestinationIP": "IP di Destinazione", + "editInternalResourceDialogDestinationPort": "Porta di Destinazione", + "editInternalResourceDialogCancel": "Annulla", + "editInternalResourceDialogSaveResource": "Salva Risorsa", + "editInternalResourceDialogSuccess": "Successo", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Risorsa interna aggiornata con successo", + "editInternalResourceDialogError": "Errore", + "editInternalResourceDialogFailedToUpdateInternalResource": "Impossibile aggiornare la risorsa interna", + "editInternalResourceDialogNameRequired": "Il nome è obbligatorio", + "editInternalResourceDialogNameMaxLength": "Il nome deve essere inferiore a 255 caratteri", + "editInternalResourceDialogProxyPortMin": "La porta proxy deve essere almeno 1", + "editInternalResourceDialogProxyPortMax": "La porta proxy deve essere inferiore a 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Formato dell'indirizzo IP non valido", + "editInternalResourceDialogDestinationPortMin": "La porta di destinazione deve essere almeno 1", + "editInternalResourceDialogDestinationPortMax": "La porta di destinazione deve essere inferiore a 65536", + "createInternalResourceDialogNoSitesAvailable": "Nessun Sito Disponibile", + "createInternalResourceDialogNoSitesAvailableDescription": "Devi avere almeno un sito Newt con una subnet configurata per creare risorse interne.", + "createInternalResourceDialogClose": "Chiudi", + "createInternalResourceDialogCreateClientResource": "Crea Risorsa Client", + "createInternalResourceDialogCreateClientResourceDescription": "Crea una nuova risorsa che sarà accessibile ai client connessi al sito selezionato.", + "createInternalResourceDialogResourceProperties": "Proprietà della Risorsa", + "createInternalResourceDialogName": "Nome", + "createInternalResourceDialogSite": "Sito", + "createInternalResourceDialogSelectSite": "Seleziona sito...", + "createInternalResourceDialogSearchSites": "Cerca siti...", + "createInternalResourceDialogNoSitesFound": "Nessun sito trovato.", + "createInternalResourceDialogProtocol": "Protocollo", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Porta del Sito", + "createInternalResourceDialogSitePortDescription": "Usa questa porta per accedere alla risorsa nel sito quando sei connesso con un client.", + "createInternalResourceDialogTargetConfiguration": "Configurazione Target", + "createInternalResourceDialogDestinationIP": "IP di Destinazione", + "createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP della risorsa sulla rete del sito.", + "createInternalResourceDialogDestinationPort": "Porta di Destinazione", + "createInternalResourceDialogDestinationPortDescription": "La porta sull'IP di destinazione dove la risorsa è accessibile.", + "createInternalResourceDialogCancel": "Annulla", + "createInternalResourceDialogCreateResource": "Crea Risorsa", + "createInternalResourceDialogSuccess": "Successo", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Risorsa interna creata con successo", + "createInternalResourceDialogError": "Errore", + "createInternalResourceDialogFailedToCreateInternalResource": "Impossibile creare la risorsa interna", + "createInternalResourceDialogNameRequired": "Il nome è obbligatorio", + "createInternalResourceDialogNameMaxLength": "Il nome non deve superare i 255 caratteri", + "createInternalResourceDialogPleaseSelectSite": "Si prega di selezionare un sito", + "createInternalResourceDialogProxyPortMin": "La porta proxy deve essere almeno 1", + "createInternalResourceDialogProxyPortMax": "La porta proxy deve essere inferiore a 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Formato dell'indirizzo IP non valido", + "createInternalResourceDialogDestinationPortMin": "La porta di destinazione deve essere almeno 1", + "createInternalResourceDialogDestinationPortMax": "La porta di destinazione deve essere inferiore a 65536", + "siteConfiguration": "Configurazione", + "siteAcceptClientConnections": "Accetta Connessioni Client", + "siteAcceptClientConnectionsDescription": "Permetti ad altri dispositivi di connettersi attraverso questa istanza Newt come gateway utilizzando i client.", + "siteAddress": "Indirizzo del Sito", + "siteAddressDescription": "Specifica l'indirizzo IP dell'host a cui i client si collegano. Questo è l'indirizzo interno del sito nella rete Pangolin per indirizzare i client. Deve rientrare nella subnet dell'Organizzazione.", + "autoLoginExternalIdp": "Accesso Automatico con IDP Esterno", + "autoLoginExternalIdpDescription": "Reindirizzare immediatamente l'utente all'IDP esterno per l'autenticazione.", + "selectIdp": "Seleziona IDP", + "selectIdpPlaceholder": "Scegli un IDP...", + "selectIdpRequired": "Si prega di selezionare un IDP quando l'accesso automatico è abilitato.", + "autoLoginTitle": "Reindirizzamento", + "autoLoginDescription": "Reindirizzandoti al provider di identità esterno per l'autenticazione.", + "autoLoginProcessing": "Preparazione dell'autenticazione...", + "autoLoginRedirecting": "Reindirizzamento al login...", + "autoLoginError": "Errore di Accesso Automatico", + "autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.", + "autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione." } From 82b49216022c1fce620177f94e2fe51f9d7cd1cf Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:50:58 -0700 Subject: [PATCH 212/219] New translations en-us.json (Korean) --- messages/ko-KR.json | 364 ++++++++++++++++++++++---------------------- 1 file changed, 182 insertions(+), 182 deletions(-) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 5fa81338..35f86baf 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.", "siteWg": "기본 WireGuard", "siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.", "siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.", "siteSeeAll": "모든 사이트 보기", "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요", "siteNewtCredentials": "Newt 자격 증명", @@ -168,7 +168,7 @@ "siteSelect": "사이트 선택", "siteSearch": "사이트 검색", "siteNotFound": "사이트를 찾을 수 없습니다.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.", "resourceType": "리소스 유형", "resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요", "resourceHTTPSSettings": "HTTPS 설정", @@ -199,7 +199,7 @@ "general": "일반", "generalSettings": "일반 설정", "proxy": "프록시", - "internal": "Internal", + "internal": "내부", "rules": "규칙", "resourceSettingDescription": "리소스의 설정을 구성하세요.", "resourceSetting": "{resourceName} 설정", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.", "targetTlsSubmit": "설정 저장", "targets": "대상 구성", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "사용자 백엔드 서비스로 트래픽을 라우팅할 대상을 설정하십시오.", "targetStickySessions": "스티키 세션 활성화", "targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.", "methodSelect": "선택 방법", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다", "pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.", "passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "비밀번호 요구사항:", + "passwordRequirementLength": "최소 8자 이상", + "passwordRequirementUppercase": "최소 대문자 하나", + "passwordRequirementLowercase": "최소 소문자 하나", + "passwordRequirementNumber": "최소 숫자 하나", + "passwordRequirementSpecial": "최소 특수 문자 하나", + "passwordRequirementsMet": "✓ 비밀번호가 모든 요구사항을 충족합니다.", + "passwordStrength": "비밀번호 강도", + "passwordStrengthWeak": "약함", + "passwordStrengthMedium": "보통", + "passwordStrengthStrong": "강함", + "passwordRequirements": "요구 사항:", + "passwordRequirementLengthText": "8자 이상", + "passwordRequirementUppercaseText": "대문자 (A-Z)", + "passwordRequirementLowercaseText": "소문자 (a-z)", + "passwordRequirementNumberText": "숫자 (0-9)", + "passwordRequirementSpecialText": "특수 문자 (!@#$%...)", + "passwordsDoNotMatch": "비밀번호가 일치하지 않습니다.", "otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다", "otpEmailSent": "OTP 전송됨", "otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.", @@ -973,7 +973,7 @@ "logoutError": "로그아웃 중 오류 발생", "signingAs": "로그인한 사용자", "serverAdmin": "서버 관리자", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "관리 자체 호스팅", "otpEnable": "이중 인증 활성화", "otpDisable": "이중 인증 비활성화", "logout": "로그 아웃", @@ -989,9 +989,9 @@ "actionDeleteSite": "사이트 삭제", "actionGetSite": "사이트 가져오기", "actionListSites": "사이트 목록", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "설정 토큰", + "setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.", + "setupTokenRequired": "설정 토큰이 필요합니다", "actionUpdateSite": "사이트 업데이트", "actionListSiteRoles": "허용된 사이트 역할 목록", "actionCreateResource": "리소스 생성", @@ -1047,11 +1047,11 @@ "actionDeleteIdpOrg": "IDP 조직 정책 삭제", "actionListIdpOrgs": "IDP 조직 목록", "actionUpdateIdpOrg": "IDP 조직 업데이트", - "actionCreateClient": "Create Client", - "actionDeleteClient": "Delete Client", - "actionUpdateClient": "Update Client", - "actionListClients": "List Clients", - "actionGetClient": "Get Client", + "actionCreateClient": "클라이언트 생성", + "actionDeleteClient": "클라이언트 삭제", + "actionUpdateClient": "클라이언트 업데이트", + "actionListClients": "클라이언트 목록", + "actionGetClient": "클라이언트 가져오기", "noneSelected": "선택된 항목 없음", "orgNotFound2": "조직이 없습니다.", "searchProgress": "검색...", @@ -1123,7 +1123,7 @@ "sidebarAllUsers": "모든 사용자", "sidebarIdentityProviders": "신원 공급자", "sidebarLicense": "라이선스", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "클라이언트 (Beta)", "sidebarDomains": "도메인", "enableDockerSocket": "Docker 소켓 활성화", "enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", @@ -1191,7 +1191,7 @@ "selectDomainTypeCnameName": "단일 도메인 (CNAME)", "selectDomainTypeCnameDescription": "단일 하위 도메인 또는 특정 도메인 항목에 사용됩니다.", "selectDomainTypeWildcardName": "와일드카드 도메인", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "이 도메인 및 그 하위 도메인.", "domainDelegation": "단일 도메인", "selectType": "유형 선택", "actions": "작업", @@ -1225,17 +1225,17 @@ "sidebarExpand": "확장하기", "newtUpdateAvailable": "업데이트 가능", "newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "도메인", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp", - "domainPickerDescription": "Enter the full domain of the resource to see available options.", - "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", + "domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.", + "domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.", "domainPickerTabAll": "모두", "domainPickerTabOrganization": "조직", "domainPickerTabProvided": "제공 됨", "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "가용성을 확인 중...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "일치하는 도메인을 찾을 수 없습니다. 다른 도메인을 시도하거나 조직의 도메인 설정을 확인하십시오.", "domainPickerOrganizationDomains": "조직 도메인", "domainPickerProvidedDomains": "제공된 도메인", "domainPickerSubdomain": "서브도메인: {subdomain}", @@ -1261,7 +1261,7 @@ "securityKeyRemoveSuccess": "보안 키가 성공적으로 제거되었습니다", "securityKeyRemoveError": "보안 키 제거 실패", "securityKeyLoadError": "보안 키를 불러오는 데 실패했습니다", - "securityKeyLogin": "Continue with security key", + "securityKeyLogin": "보안 키로 계속하기", "securityKeyAuthError": "보안 키를 사용한 인증 실패", "securityKeyRecommendation": "항상 계정에 액세스할 수 있도록 다른 장치에 백업 보안 키를 등록하세요.", "registering": "등록 중...", @@ -1295,7 +1295,7 @@ "createDomainName": "이름:", "createDomainValue": "값:", "createDomainCnameRecords": "CNAME 레코드", - "createDomainARecords": "A Records", + "createDomainARecords": "A 레코드", "createDomainRecordNumber": "레코드 {number}", "createDomainTxtRecords": "TXT 레코드", "createDomainSaveTheseRecords": "이 레코드 저장", @@ -1305,150 +1305,150 @@ "resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다", "resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "동의합니다", + "termsOfService": "서비스 약관", + "and": "및", + "privacyPolicy": "개인 정보 보호 정책" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", + "siteRequired": "사이트가 필요합니다.", + "olmTunnel": "Olm 터널", + "olmTunnelDescription": "클라이언트 연결에 Olm 사용", + "errorCreatingClient": "클라이언트 생성 오류", + "clientDefaultsNotFound": "클라이언트 기본값을 찾을 수 없습니다.", + "createClient": "클라이언트 생성", + "createClientDescription": "사이트에 연결하기 위한 새 클라이언트를 생성하십시오.", + "seeAllClients": "모든 클라이언트 보기", + "clientInformation": "클라이언트 정보", + "clientNamePlaceholder": "클라이언트 이름", + "address": "주소", + "subnetPlaceholder": "서브넷", + "addressDescription": "이 클라이언트가 연결에 사용할 주소", + "selectSites": "사이트 선택", + "sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.", + "clientInstallOlm": "Olm 설치", + "clientInstallOlmDescription": "시스템에서 Olm을 실행하기", + "clientOlmCredentials": "Olm 자격 증명", + "clientOlmCredentialsDescription": "Olm이 서버와 인증하는 방법입니다.", + "olmEndpoint": "Olm 엔드포인트", "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "olmSecretKey": "Olm 비밀 키", + "clientCredentialsSave": "자격 증명 저장", + "clientCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", + "generalSettingsDescription": "이 클라이언트에 대한 일반 설정을 구성하세요.", + "clientUpdated": "클라이언트 업데이트됨", + "clientUpdatedDescription": "클라이언트가 업데이트되었습니다.", + "clientUpdateFailed": "클라이언트 업데이트 실패", + "clientUpdateError": "클라이언트 업데이트 중 오류가 발생했습니다.", + "sitesFetchFailed": "사이트 가져오기 실패", + "sitesFetchError": "사이트 가져오는 중 오류가 발생했습니다.", + "olmErrorFetchReleases": "Olm 릴리즈 가져오는 중 오류가 발생했습니다.", + "olmErrorFetchLatest": "최신 Olm 릴리즈 가져오는 중 오류가 발생했습니다.", + "remoteSubnets": "원격 서브넷", + "enterCidrRange": "CIDR 범위 입력", + "remoteSubnetsDescription": "이 사이트에서 원격으로 액세스할 수 있는 CIDR 범위를 추가하세요. 10.0.0.0/24와 같은 형식을 사용하세요. 이는 VPN 클라이언트 연결에만 적용됩니다.", + "resourceEnableProxy": "공개 프록시 사용", + "resourceEnableProxyDescription": "이 리소스에 대한 공개 프록시를 활성화하십시오. 이를 통해 네트워크 외부로부터 클라우드를 통해 열린 포트에서 리소스에 액세스할 수 있습니다. Traefik 구성이 필요합니다.", + "externalProxyEnabled": "외부 프록시 활성화됨", + "addNewTarget": "새 대상 추가", + "targetsList": "대상 목록", + "targetErrorDuplicateTargetFound": "중복 대상 발견", + "httpMethod": "HTTP 메소드", + "selectHttpMethod": "HTTP 메소드 선택", + "domainPickerSubdomainLabel": "서브도메인", + "domainPickerBaseDomainLabel": "기본 도메인", + "domainPickerSearchDomains": "도메인 검색...", + "domainPickerNoDomainsFound": "찾을 수 없는 도메인이 없습니다", + "domainPickerLoadingDomains": "도메인 로딩 중...", + "domainPickerSelectBaseDomain": "기본 도메인 선택...", + "domainPickerNotAvailableForCname": "CNAME 도메인에는 사용할 수 없습니다", + "domainPickerEnterSubdomainOrLeaveBlank": "서브도메인을 입력하거나 기본 도메인을 사용하려면 공백으로 두십시오.", + "domainPickerEnterSubdomainToSearch": "사용 가능한 무료 도메인에서 검색 및 선택할 서브도메인 입력.", + "domainPickerFreeDomains": "무료 도메인", + "domainPickerSearchForAvailableDomains": "사용 가능한 도메인 검색", + "resourceDomain": "도메인", + "resourceEditDomain": "도메인 수정", + "siteName": "사이트 이름", + "proxyPort": "포트", + "resourcesTableProxyResources": "프록시 리소스", + "resourcesTableClientResources": "클라이언트 리소스", + "resourcesTableNoProxyResourcesFound": "프록시 리소스를 찾을 수 없습니다.", + "resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.", + "resourcesTableDestination": "대상지", + "resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.", + "resourcesTableClients": "클라이언트", + "resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.", + "editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정", + "editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.", + "editInternalResourceDialogResourceProperties": "리소스 속성", + "editInternalResourceDialogName": "이름", + "editInternalResourceDialogProtocol": "프로토콜", + "editInternalResourceDialogSitePort": "사이트 포트", + "editInternalResourceDialogTargetConfiguration": "대상 구성", + "editInternalResourceDialogDestinationIP": "대상 IP", + "editInternalResourceDialogDestinationPort": "대상 IP의 포트", + "editInternalResourceDialogCancel": "취소", + "editInternalResourceDialogSaveResource": "리소스 저장", + "editInternalResourceDialogSuccess": "성공", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "내부 리소스가 성공적으로 업데이트되었습니다", + "editInternalResourceDialogError": "오류", + "editInternalResourceDialogFailedToUpdateInternalResource": "내부 리소스 업데이트 실패", + "editInternalResourceDialogNameRequired": "이름은 필수입니다.", + "editInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.", + "editInternalResourceDialogProxyPortMin": "프록시 포트는 최소 1이어야 합니다.", + "editInternalResourceDialogProxyPortMax": "프록시 포트는 65536 미만이어야 합니다.", + "editInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식", + "editInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.", + "editInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.", + "createInternalResourceDialogNoSitesAvailable": "사용 가능한 사이트가 없습니다.", + "createInternalResourceDialogNoSitesAvailableDescription": "내부 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.", + "createInternalResourceDialogClose": "닫기", + "createInternalResourceDialogCreateClientResource": "클라이언트 리소스 생성", + "createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다.", + "createInternalResourceDialogResourceProperties": "리소스 속성", + "createInternalResourceDialogName": "이름", + "createInternalResourceDialogSite": "사이트", + "createInternalResourceDialogSelectSite": "사이트 선택...", + "createInternalResourceDialogSearchSites": "사이트 검색...", + "createInternalResourceDialogNoSitesFound": "사이트를 찾을 수 없습니다.", + "createInternalResourceDialogProtocol": "프로토콜", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "사이트 포트", + "createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.", + "createInternalResourceDialogTargetConfiguration": "대상 설정", + "createInternalResourceDialogDestinationIP": "대상 IP", + "createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 주소입니다.", + "createInternalResourceDialogDestinationPort": "대상 포트", + "createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.", + "createInternalResourceDialogCancel": "취소", + "createInternalResourceDialogCreateResource": "리소스 생성", + "createInternalResourceDialogSuccess": "성공", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "내부 리소스가 성공적으로 생성되었습니다.", + "createInternalResourceDialogError": "오류", + "createInternalResourceDialogFailedToCreateInternalResource": "내부 리소스 생성 실패", + "createInternalResourceDialogNameRequired": "이름은 필수입니다.", + "createInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.", + "createInternalResourceDialogPleaseSelectSite": "사이트를 선택하세요", + "createInternalResourceDialogProxyPortMin": "프록시 포트는 최소 1이어야 합니다.", + "createInternalResourceDialogProxyPortMax": "프록시 포트는 65536 미만이어야 합니다.", + "createInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식", + "createInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.", + "createInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.", + "siteConfiguration": "설정", + "siteAcceptClientConnections": "클라이언트 연결 허용", + "siteAcceptClientConnectionsDescription": "이 Newt 인스턴스를 게이트웨이로 사용하여 다른 장치가 연결될 수 있도록 허용합니다.", + "siteAddress": "사이트 주소", + "siteAddressDescription": "클라이언트가 연결하기 위한 호스트의 IP 주소를 지정합니다. 이는 클라이언트가 주소를 지정하기 위한 Pangolin 네트워크의 사이트 내부 주소입니다. 조직 서브넷 내에 있어야 합니다.", + "autoLoginExternalIdp": "외부 IDP로 자동 로그인", + "autoLoginExternalIdpDescription": "인증을 위해 외부 IDP로 사용자를 즉시 리디렉션합니다.", + "selectIdp": "IDP 선택", + "selectIdpPlaceholder": "IDP 선택...", + "selectIdpRequired": "자동 로그인이 활성화된 경우 IDP를 선택하십시오.", + "autoLoginTitle": "리디렉션 중", + "autoLoginDescription": "인증을 위해 외부 ID 공급자로 리디렉션 중입니다.", + "autoLoginProcessing": "인증 준비 중...", + "autoLoginRedirecting": "로그인으로 리디렉션 중...", + "autoLoginError": "자동 로그인 오류", + "autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.", + "autoLoginErrorGeneratingUrl": "인증 URL 생성 실패." } From 2b4a39e64ca078602106f44b7eefe803e5a346c4 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:50:59 -0700 Subject: [PATCH 213/219] New translations en-us.json (Dutch) --- messages/nl-NL.json | 248 ++++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 06d2f121..69c70083 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.", "siteWg": "Basis WireGuard", "siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Gebruik elke WireGuard-client om een tunnel op te zetten. Handmatige NAT-instelling vereist. WERKT ALLEEN OP SELF HOSTED NODES", "siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES", "siteSeeAll": "Alle werkruimtes bekijken", "siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site", "siteNewtCredentials": "Nieuwste aanmeldgegevens", @@ -168,7 +168,7 @@ "siteSelect": "Selecteer site", "siteSearch": "Zoek site", "siteNotFound": "Geen site gevonden.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Deze site zal connectiviteit met het doelwit bieden.", "resourceType": "Type bron", "resourceTypeDescription": "Bepaal hoe u toegang wilt krijgen tot uw bron", "resourceHTTPSSettings": "HTTPS instellingen", @@ -199,7 +199,7 @@ "general": "Algemeen", "generalSettings": "Algemene instellingen", "proxy": "Proxy", - "internal": "Internal", + "internal": "Intern", "rules": "Regels", "resourceSettingDescription": "Configureer de instellingen op uw bron", "resourceSetting": "{resourceName} instellingen", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.", "targetTlsSubmit": "Instellingen opslaan", "targets": "Doelstellingen configuratie", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Stel doelen in om verkeer naar uw backend-services te leiden", "targetStickySessions": "Sticky sessies inschakelen", "targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.", "methodSelect": "Selecteer methode", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn", "pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten", "passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Wachtwoordvereisten:", + "passwordRequirementLength": "Minstens 8 tekens lang", + "passwordRequirementUppercase": "Minstens één hoofdletter", + "passwordRequirementLowercase": "Minstens één kleine letter", + "passwordRequirementNumber": "Minstens één cijfer", + "passwordRequirementSpecial": "Minstens één speciaal teken", + "passwordRequirementsMet": "✓ Wachtwoord voldoet aan alle vereisten", + "passwordStrength": "Wachtwoord sterkte", + "passwordStrengthWeak": "Zwak", + "passwordStrengthMedium": "Gemiddeld", + "passwordStrengthStrong": "Sterk", + "passwordRequirements": "Vereisten:", + "passwordRequirementLengthText": "8+ tekens", + "passwordRequirementUppercaseText": "Hoofdletter (A-Z)", + "passwordRequirementLowercaseText": "Kleine letter (a-z)", + "passwordRequirementNumberText": "Cijfer (0-9)", + "passwordRequirementSpecialText": "Speciaal teken (!@#$%...)", + "passwordsDoNotMatch": "Wachtwoorden komen niet overeen", "otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn", "otpEmailSent": "OTP verzonden", "otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden", @@ -973,7 +973,7 @@ "logoutError": "Fout bij uitloggen", "signingAs": "Ingelogd als", "serverAdmin": "Server Beheerder", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Beheerde Self-Hosted", "otpEnable": "Twee-factor inschakelen", "otpDisable": "Tweestapsverificatie uitschakelen", "logout": "Log uit", @@ -990,8 +990,8 @@ "actionGetSite": "Site ophalen", "actionListSites": "Sites weergeven", "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.", + "setupTokenRequired": "Setup-token is vereist", "actionUpdateSite": "Site bijwerken", "actionListSiteRoles": "Toon toegestane sitenollen", "actionCreateResource": "Bron maken", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.", "remoteSubnets": "Externe Subnets", "enterCidrRange": "Voer CIDR-bereik in", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Voeg CIDR-bereiken toe die vanaf deze site op afstand toegankelijk zijn met behulp van clients. Gebruik een formaat zoals 10.0.0.0/24. Dit geldt ALLEEN voor VPN-clientconnectiviteit.", "resourceEnableProxy": "Openbare proxy inschakelen", "resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.", "externalProxyEnabled": "Externe Proxy Ingeschakeld", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", + "addNewTarget": "Voeg nieuw doelwit toe", + "targetsList": "Lijst met doelen", + "targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden", + "httpMethod": "HTTP-methode", + "selectHttpMethod": "Selecteer HTTP-methode", + "domainPickerSubdomainLabel": "Subdomein", + "domainPickerBaseDomainLabel": "Basisdomein", + "domainPickerSearchDomains": "Zoek domeinen...", + "domainPickerNoDomainsFound": "Geen domeinen gevonden", + "domainPickerLoadingDomains": "Domeinen laden...", + "domainPickerSelectBaseDomain": "Selecteer basisdomein...", + "domainPickerNotAvailableForCname": "Niet beschikbaar voor CNAME-domeinen", + "domainPickerEnterSubdomainOrLeaveBlank": "Voer een subdomein in of laat leeg om basisdomein te gebruiken.", + "domainPickerEnterSubdomainToSearch": "Voer een subdomein in om te zoeken en te selecteren uit beschikbare gratis domeinen.", + "domainPickerFreeDomains": "Gratis Domeinen", + "domainPickerSearchForAvailableDomains": "Zoek naar beschikbare domeinen", + "resourceDomain": "Domein", + "resourceEditDomain": "Domein bewerken", + "siteName": "Site Naam", + "proxyPort": "Poort", + "resourcesTableProxyResources": "Proxybronnen", + "resourcesTableClientResources": "Clientbronnen", + "resourcesTableNoProxyResourcesFound": "Geen proxybronnen gevonden.", + "resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.", + "resourcesTableDestination": "Bestemming", + "resourcesTableTheseResourcesForUseWith": "Deze bronnen zijn bedoeld voor gebruik met", + "resourcesTableClients": "Clienten", + "resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.", + "editInternalResourceDialogEditClientResource": "Bewerk clientbron", + "editInternalResourceDialogUpdateResourceProperties": "Werk de eigenschapen van de bron en doelconfiguratie bij voor {resourceName}.", + "editInternalResourceDialogResourceProperties": "Bron eigenschappen", + "editInternalResourceDialogName": "Naam", "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", + "editInternalResourceDialogSitePort": "Site Poort", + "editInternalResourceDialogTargetConfiguration": "Doelconfiguratie", + "editInternalResourceDialogDestinationIP": "Bestemming IP", + "editInternalResourceDialogDestinationPort": "Bestemmingspoort", + "editInternalResourceDialogCancel": "Annuleren", + "editInternalResourceDialogSaveResource": "Sla bron op", + "editInternalResourceDialogSuccess": "Succes", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne bron succesvol bijgewerkt", + "editInternalResourceDialogError": "Fout", + "editInternalResourceDialogFailedToUpdateInternalResource": "Het bijwerken van de interne bron is mislukt", + "editInternalResourceDialogNameRequired": "Naam is verplicht", + "editInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens", + "editInternalResourceDialogProxyPortMin": "Proxy poort moet minstens 1 zijn", + "editInternalResourceDialogProxyPortMax": "Proxy poort moet minder dan 65536 zijn", + "editInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat", + "editInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn", + "editInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn", + "createInternalResourceDialogNoSitesAvailable": "Geen sites beschikbaar", + "createInternalResourceDialogNoSitesAvailableDescription": "U moet ten minste één Newt-site hebben met een geconfigureerd subnet om interne bronnen aan te maken.", + "createInternalResourceDialogClose": "Sluiten", + "createInternalResourceDialogCreateClientResource": "Maak clientbron", + "createInternalResourceDialogCreateClientResourceDescription": "Maak een nieuwe bron die toegankelijk zal zijn voor clients die verbonden zijn met de geselecteerde site.", + "createInternalResourceDialogResourceProperties": "Bron-eigenschappen", + "createInternalResourceDialogName": "Naam", "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogSelectSite": "Selecteer site...", + "createInternalResourceDialogSearchSites": "Zoek sites...", + "createInternalResourceDialogNoSitesFound": "Geen sites gevonden.", "createInternalResourceDialogProtocol": "Protocol", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Site Poort", + "createInternalResourceDialogSitePortDescription": "Gebruik deze poort om toegang te krijgen tot de bron op de site wanneer verbonden met een client.", + "createInternalResourceDialogTargetConfiguration": "Doelconfiguratie", + "createInternalResourceDialogDestinationIP": "Bestemming IP", + "createInternalResourceDialogDestinationIPDescription": "Het IP-adres van de bron op het netwerk van de site.", + "createInternalResourceDialogDestinationPort": "Bestemmingspoort", + "createInternalResourceDialogDestinationPortDescription": "De poort op het bestemmings-IP waar de bron toegankelijk is.", + "createInternalResourceDialogCancel": "Annuleren", + "createInternalResourceDialogCreateResource": "Bron aanmaken", + "createInternalResourceDialogSuccess": "Succes", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne bron succesvol aangemaakt", + "createInternalResourceDialogError": "Fout", + "createInternalResourceDialogFailedToCreateInternalResource": "Het aanmaken van de interne bron is mislukt", + "createInternalResourceDialogNameRequired": "Naam is verplicht", + "createInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens", + "createInternalResourceDialogPleaseSelectSite": "Selecteer alstublieft een site", + "createInternalResourceDialogProxyPortMin": "Proxy poort moet minstens 1 zijn", + "createInternalResourceDialogProxyPortMax": "Proxy poort moet minder dan 65536 zijn", + "createInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat", + "createInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn", + "createInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn", + "siteConfiguration": "Configuratie", + "siteAcceptClientConnections": "Accepteer clientverbindingen", + "siteAcceptClientConnectionsDescription": "Sta toe dat andere apparaten verbinding maken via deze Newt-instantie als een gateway met behulp van clients.", + "siteAddress": "Siteadres", + "siteAddressDescription": "Specificeren het IP-adres van de host voor clients om verbinding mee te maken. Dit is het interne adres van de site in het Pangolin netwerk voor clients om te adresseren. Moet binnen het Organisatienetwerk vallen.", + "autoLoginExternalIdp": "Auto Login met Externe IDP", + "autoLoginExternalIdpDescription": "De gebruiker onmiddellijk doorsturen naar de externe IDP voor authenticatie.", + "selectIdp": "Selecteer IDP", + "selectIdpPlaceholder": "Kies een IDP...", + "selectIdpRequired": "Selecteer alstublieft een IDP wanneer automatisch inloggen is ingeschakeld.", + "autoLoginTitle": "Omleiden", + "autoLoginDescription": "Je wordt doorverwezen naar de externe identity provider voor authenticatie.", + "autoLoginProcessing": "Authenticatie voorbereiden...", + "autoLoginRedirecting": "Redirecting naar inloggen...", + "autoLoginError": "Auto Login Fout", + "autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.", + "autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt." } From a6a909ae4f3c3be28a1e1ac22405d2c7a2e339ee Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:51:00 -0700 Subject: [PATCH 214/219] New translations en-us.json (Polish) --- messages/pl-PL.json | 254 ++++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 127 deletions(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index ab5aff78..a23e634a 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Łatwiejszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.", "siteWg": "Podstawowy WireGuard", "siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana ręczna konfiguracja NAT. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH", "siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Tylko zasoby lokalne. Brak tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH", "siteSeeAll": "Zobacz wszystkie witryny", "siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną", "siteNewtCredentials": "Aktualne dane logowania", @@ -168,7 +168,7 @@ "siteSelect": "Wybierz witrynę", "siteSearch": "Szukaj witryny", "siteNotFound": "Nie znaleziono witryny.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Ta strona zapewni połączenie z celem.", "resourceType": "Typ zasobu", "resourceTypeDescription": "Określ jak chcesz uzyskać dostęp do swojego zasobu", "resourceHTTPSSettings": "Ustawienia HTTPS", @@ -199,7 +199,7 @@ "general": "Ogólny", "generalSettings": "Ustawienia ogólne", "proxy": "Serwer pośredniczący", - "internal": "Internal", + "internal": "Wewętrzny", "rules": "Regulamin", "resourceSettingDescription": "Skonfiguruj ustawienia zasobu", "resourceSetting": "Ustawienia {resourceName}", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.", "targetTlsSubmit": "Zapisz ustawienia", "targets": "Konfiguracja celów", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Skonfiguruj cele do kierowania ruchu do usług zaplecza", "targetStickySessions": "Włącz sesje trwałe", "targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.", "methodSelect": "Wybierz metodę", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr", "pincodeRequirementsChars": "PIN może zawierać tylko cyfry", "passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Wymagania dotyczące hasła:", + "passwordRequirementLength": "Przynajmniej 8 znaków długości", + "passwordRequirementUppercase": "Przynajmniej jedna wielka litera", + "passwordRequirementLowercase": "Przynajmniej jedna mała litera", + "passwordRequirementNumber": "Przynajmniej jedna cyfra", + "passwordRequirementSpecial": "Przynajmniej jeden znak specjalny", + "passwordRequirementsMet": "✓ Hasło spełnia wszystkie wymagania", + "passwordStrength": "Siła hasła", + "passwordStrengthWeak": "Słabe", + "passwordStrengthMedium": "Średnie", + "passwordStrengthStrong": "Silne", + "passwordRequirements": "Wymagania:", + "passwordRequirementLengthText": "8+ znaków", + "passwordRequirementUppercaseText": "Wielka litera (A-Z)", + "passwordRequirementLowercaseText": "Mała litera (a-z)", + "passwordRequirementNumberText": "Cyfra (0-9)", + "passwordRequirementSpecialText": "Znak specjalny (!@#$%...)", + "passwordsDoNotMatch": "Hasła nie są zgodne", "otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak", "otpEmailSent": "Kod jednorazowy wysłany", "otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail", @@ -973,7 +973,7 @@ "logoutError": "Błąd podczas wylogowywania", "signingAs": "Zalogowany jako", "serverAdmin": "Administrator serwera", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Zarządzane Samodzielnie-Hostingowane", "otpEnable": "Włącz uwierzytelnianie dwuskładnikowe", "otpDisable": "Wyłącz uwierzytelnianie dwuskładnikowe", "logout": "Wyloguj się", @@ -989,9 +989,9 @@ "actionDeleteSite": "Usuń witrynę", "actionGetSite": "Pobierz witrynę", "actionListSites": "Lista witryn", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Skonfiguruj token", + "setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.", + "setupTokenRequired": "Wymagany jest token konfiguracji", "actionUpdateSite": "Aktualizuj witrynę", "actionListSiteRoles": "Lista dozwolonych ról witryny", "actionCreateResource": "Utwórz zasób", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.", "remoteSubnets": "Zdalne Podsieci", "enterCidrRange": "Wprowadź zakres CIDR", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Dodaj zakresy CIDR, które można uzyskać zdalnie z tej strony za pomocą klientów. Użyj formatu jak 10.0.0.0/24. Dotyczy to WYŁĄCZNIE łączności klienta VPN.", "resourceEnableProxy": "Włącz publiczny proxy", "resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.", "externalProxyEnabled": "Zewnętrzny Proxy Włączony", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", + "addNewTarget": "Dodaj nowy cel", + "targetsList": "Lista celów", + "targetErrorDuplicateTargetFound": "Znaleziono duplikat celu", + "httpMethod": "Metoda HTTP", + "selectHttpMethod": "Wybierz metodę HTTP", + "domainPickerSubdomainLabel": "Poddomena", + "domainPickerBaseDomainLabel": "Domen bazowa", + "domainPickerSearchDomains": "Szukaj domen...", + "domainPickerNoDomainsFound": "Nie znaleziono domen", + "domainPickerLoadingDomains": "Ładowanie domen...", + "domainPickerSelectBaseDomain": "Wybierz domenę bazową...", + "domainPickerNotAvailableForCname": "Niedostępne dla domen CNAME", + "domainPickerEnterSubdomainOrLeaveBlank": "Wprowadź poddomenę lub pozostaw puste, aby użyć domeny bazowej.", + "domainPickerEnterSubdomainToSearch": "Wprowadź poddomenę, aby wyszukać i wybrać z dostępnych darmowych domen.", + "domainPickerFreeDomains": "Darmowe domeny", + "domainPickerSearchForAvailableDomains": "Szukaj dostępnych domen", + "resourceDomain": "Domena", + "resourceEditDomain": "Edytuj domenę", + "siteName": "Nazwa strony", "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "resourcesTableProxyResources": "Zasoby proxy", + "resourcesTableClientResources": "Zasoby klienta", + "resourcesTableNoProxyResourcesFound": "Nie znaleziono zasobów proxy.", + "resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.", + "resourcesTableDestination": "Miejsce docelowe", + "resourcesTableTheseResourcesForUseWith": "Te zasoby są do użytku z", + "resourcesTableClients": "Klientami", + "resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.", + "editInternalResourceDialogEditClientResource": "Edytuj zasób klienta", + "editInternalResourceDialogUpdateResourceProperties": "Zaktualizuj właściwości zasobu i konfigurację celu dla {resourceName}.", + "editInternalResourceDialogResourceProperties": "Właściwości zasobów", + "editInternalResourceDialogName": "Nazwa", + "editInternalResourceDialogProtocol": "Protokół", + "editInternalResourceDialogSitePort": "Port witryny", + "editInternalResourceDialogTargetConfiguration": "Konfiguracja celu", + "editInternalResourceDialogDestinationIP": "IP docelowe", + "editInternalResourceDialogDestinationPort": "Port docelowy", + "editInternalResourceDialogCancel": "Anuluj", + "editInternalResourceDialogSaveResource": "Zapisz zasób", + "editInternalResourceDialogSuccess": "Sukces", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Wewnętrzny zasób zaktualizowany pomyślnie", + "editInternalResourceDialogError": "Błąd", + "editInternalResourceDialogFailedToUpdateInternalResource": "Nie udało się zaktualizować wewnętrznego zasobu", + "editInternalResourceDialogNameRequired": "Nazwa jest wymagana", + "editInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków", + "editInternalResourceDialogProxyPortMin": "Port proxy musi wynosić przynajmniej 1", + "editInternalResourceDialogProxyPortMax": "Port proxy nie może być większy niż 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP", + "editInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1", + "editInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536", + "createInternalResourceDialogNoSitesAvailable": "Brak dostępnych stron", + "createInternalResourceDialogNoSitesAvailableDescription": "Musisz mieć co najmniej jedną stronę Newt z skonfigurowanym podsiecią, aby tworzyć wewnętrzne zasoby.", + "createInternalResourceDialogClose": "Zamknij", + "createInternalResourceDialogCreateClientResource": "Utwórz zasób klienta", + "createInternalResourceDialogCreateClientResourceDescription": "Utwórz nowy zasób, który będzie dostępny dla klientów połączonych z wybraną stroną.", + "createInternalResourceDialogResourceProperties": "Właściwości zasobów", + "createInternalResourceDialogName": "Nazwa", + "createInternalResourceDialogSite": "Witryna", + "createInternalResourceDialogSelectSite": "Wybierz stronę...", + "createInternalResourceDialogSearchSites": "Szukaj stron...", + "createInternalResourceDialogNoSitesFound": "Nie znaleziono stron.", + "createInternalResourceDialogProtocol": "Protokół", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Port witryny", + "createInternalResourceDialogSitePortDescription": "Użyj tego portu, aby uzyskać dostęp do zasobu na stronie, gdy połączony z klientem.", + "createInternalResourceDialogTargetConfiguration": "Konfiguracja celu", + "createInternalResourceDialogDestinationIP": "IP docelowe", + "createInternalResourceDialogDestinationIPDescription": "Adres IP zasobu w sieci strony.", + "createInternalResourceDialogDestinationPort": "Port docelowy", + "createInternalResourceDialogDestinationPortDescription": "Port na docelowym IP, gdzie zasób jest dostępny.", + "createInternalResourceDialogCancel": "Anuluj", + "createInternalResourceDialogCreateResource": "Utwórz zasób", + "createInternalResourceDialogSuccess": "Sukces", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Wewnętrzny zasób utworzony pomyślnie", + "createInternalResourceDialogError": "Błąd", + "createInternalResourceDialogFailedToCreateInternalResource": "Nie udało się utworzyć wewnętrznego zasobu", + "createInternalResourceDialogNameRequired": "Nazwa jest wymagana", + "createInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków", + "createInternalResourceDialogPleaseSelectSite": "Proszę wybrać stronę", + "createInternalResourceDialogProxyPortMin": "Port proxy musi wynosić przynajmniej 1", + "createInternalResourceDialogProxyPortMax": "Port proxy nie może być większy niż 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP", + "createInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1", + "createInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536", + "siteConfiguration": "Konfiguracja", + "siteAcceptClientConnections": "Akceptuj połączenia klienta", + "siteAcceptClientConnectionsDescription": "Pozwól innym urządzeniom połączyć się przez tę instancję Newt jako bramę za pomocą klientów.", + "siteAddress": "Adres strony", + "siteAddressDescription": "Podaj adres IP hosta, do którego klienci będą się łączyć. Jest to wewnętrzny adres strony w sieci Pangolin dla klientów do adresowania. Musi zawierać się w podsieci organizacji.", + "autoLoginExternalIdp": "Automatyczny login z zewnętrznym IDP", + "autoLoginExternalIdpDescription": "Natychmiastowe przekierowanie użytkownika do zewnętrznego IDP w celu uwierzytelnienia.", + "selectIdp": "Wybierz IDP", + "selectIdpPlaceholder": "Wybierz IDP...", + "selectIdpRequired": "Proszę wybrać IDP, gdy aktywne jest automatyczne logowanie.", + "autoLoginTitle": "Przekierowywanie", + "autoLoginDescription": "Przekierowanie do zewnętrznego dostawcy tożsamości w celu uwierzytelnienia.", + "autoLoginProcessing": "Przygotowywanie uwierzytelniania...", + "autoLoginRedirecting": "Przekierowanie do logowania...", + "autoLoginError": "Błąd automatycznego logowania", + "autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.", + "autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania." } From 0e14441f732d7ae25d8989a79aed7e4aeb17afcb Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:51:01 -0700 Subject: [PATCH 215/219] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 254 ++++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 127 deletions(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 96ac821e..7c27df64 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "A maneira mais fácil de criar um ponto de entrada na sua rede. Nenhuma configuração extra.", "siteWg": "WireGuard Básico", "siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Use qualquer cliente WireGuard para estabelecer um túnel. Configuração manual NAT necessária. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS", "siteLocalDescription": "Recursos locais apenas. Sem túneis.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Apenas recursos locais. Sem tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS", "siteSeeAll": "Ver todos os sites", "siteTunnelDescription": "Determine como você deseja se conectar ao seu site", "siteNewtCredentials": "Credenciais Novas", @@ -168,7 +168,7 @@ "siteSelect": "Selecionar site", "siteSearch": "Procurar no site", "siteNotFound": "Nenhum site encontrado.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Este site fornecerá conectividade ao destino.", "resourceType": "Tipo de Recurso", "resourceTypeDescription": "Determine como você deseja acessar seu recurso", "resourceHTTPSSettings": "Configurações de HTTPS", @@ -199,7 +199,7 @@ "general": "Gerais", "generalSettings": "Configurações Gerais", "proxy": "Proxy", - "internal": "Internal", + "internal": "Interno", "rules": "Regras", "resourceSettingDescription": "Configure as configurações do seu recurso", "resourceSetting": "Configurações do {resourceName}", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.", "targetTlsSubmit": "Salvar Configurações", "targets": "Configuração de Alvos", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Configure alvos para rotear tráfego para seus serviços de backend", "targetStickySessions": "Ativar Sessões Persistentes", "targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.", "methodSelect": "Selecionar método", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos", "pincodeRequirementsChars": "O PIN deve conter apenas números", "passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Requisitos de senha:", + "passwordRequirementLength": "Pelo menos 8 caracteres de comprimento", + "passwordRequirementUppercase": "Pelo menos uma letra maiúscula", + "passwordRequirementLowercase": "Pelo menos uma letra minúscula", + "passwordRequirementNumber": "Pelo menos um número", + "passwordRequirementSpecial": "Pelo menos um caractere especial", + "passwordRequirementsMet": "✓ Senha atende a todos os requisitos", + "passwordStrength": "Força da senha", + "passwordStrengthWeak": "Fraca", + "passwordStrengthMedium": "Média", + "passwordStrengthStrong": "Forte", + "passwordRequirements": "Requisitos:", + "passwordRequirementLengthText": "8+ caracteres", + "passwordRequirementUppercaseText": "Letra maiúscula (A-Z)", + "passwordRequirementLowercaseText": "Letra minúscula (a-z)", + "passwordRequirementNumberText": "Número (0-9)", + "passwordRequirementSpecialText": "Caractere especial (!@#$%...)", + "passwordsDoNotMatch": "As palavras-passe não correspondem", "otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere", "otpEmailSent": "OTP Enviado", "otpEmailSentDescription": "Um OTP foi enviado para o seu email", @@ -973,7 +973,7 @@ "logoutError": "Erro ao terminar sessão", "signingAs": "Sessão iniciada como", "serverAdmin": "Administrador do Servidor", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Gerenciado Auto-Hospedado", "otpEnable": "Ativar Autenticação de Dois Fatores", "otpDisable": "Desativar Autenticação de Dois Fatores", "logout": "Terminar Sessão", @@ -989,9 +989,9 @@ "actionDeleteSite": "Eliminar Site", "actionGetSite": "Obter Site", "actionListSites": "Listar Sites", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Configuração do Token", + "setupTokenDescription": "Digite o token de configuração do console do servidor.", + "setupTokenRequired": "Token de configuração é necessário", "actionUpdateSite": "Atualizar Site", "actionListSiteRoles": "Listar Funções Permitidas do Site", "actionCreateResource": "Criar Recurso", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.", "remoteSubnets": "Sub-redes Remotas", "enterCidrRange": "Insira o intervalo CIDR", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Adicionar intervalos CIDR que podem ser acessados deste site remotamente usando clientes. Use um formato como 10.0.0.0/24. Isso SOMENTE se aplica à conectividade do cliente VPN.", "resourceEnableProxy": "Ativar Proxy Público", "resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.", "externalProxyEnabled": "Proxy Externo Habilitado", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", + "addNewTarget": "Adicionar Novo Alvo", + "targetsList": "Lista de Alvos", + "targetErrorDuplicateTargetFound": "Alvo duplicado encontrado", + "httpMethod": "Método HTTP", + "selectHttpMethod": "Selecionar método HTTP", + "domainPickerSubdomainLabel": "Subdomínio", + "domainPickerBaseDomainLabel": "Domínio Base", + "domainPickerSearchDomains": "Buscar domínios...", + "domainPickerNoDomainsFound": "Nenhum domínio encontrado", + "domainPickerLoadingDomains": "Carregando domínios...", + "domainPickerSelectBaseDomain": "Selecione o domínio base...", + "domainPickerNotAvailableForCname": "Não disponível para domínios CNAME", + "domainPickerEnterSubdomainOrLeaveBlank": "Digite um subdomínio ou deixe em branco para usar o domínio base.", + "domainPickerEnterSubdomainToSearch": "Digite um subdomínio para buscar e selecionar entre os domínios gratuitos disponíveis.", + "domainPickerFreeDomains": "Domínios Gratuitos", + "domainPickerSearchForAvailableDomains": "Pesquise por domínios disponíveis", + "resourceDomain": "Domínio", + "resourceEditDomain": "Editar Domínio", + "siteName": "Nome do Site", + "proxyPort": "Porta", + "resourcesTableProxyResources": "Recursos de Proxy", + "resourcesTableClientResources": "Recursos do Cliente", + "resourcesTableNoProxyResourcesFound": "Nenhum recurso de proxy encontrado.", + "resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.", + "resourcesTableDestination": "Destino", + "resourcesTableTheseResourcesForUseWith": "Esses recursos são para uso com", + "resourcesTableClients": "Clientes", + "resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.", + "editInternalResourceDialogEditClientResource": "Editar Recurso do Cliente", + "editInternalResourceDialogUpdateResourceProperties": "Atualize as propriedades do recurso e a configuração do alvo para {resourceName}.", + "editInternalResourceDialogResourceProperties": "Propriedades do Recurso", + "editInternalResourceDialogName": "Nome", + "editInternalResourceDialogProtocol": "Protocolo", + "editInternalResourceDialogSitePort": "Porta do Site", + "editInternalResourceDialogTargetConfiguration": "Configuração do Alvo", + "editInternalResourceDialogDestinationIP": "IP de Destino", + "editInternalResourceDialogDestinationPort": "Porta de Destino", + "editInternalResourceDialogCancel": "Cancelar", + "editInternalResourceDialogSaveResource": "Salvar Recurso", + "editInternalResourceDialogSuccess": "Sucesso", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno atualizado com sucesso", + "editInternalResourceDialogError": "Erro", + "editInternalResourceDialogFailedToUpdateInternalResource": "Falha ao atualizar recurso interno", + "editInternalResourceDialogNameRequired": "Nome é obrigatório", + "editInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres", + "editInternalResourceDialogProxyPortMin": "Porta de proxy deve ser pelo menos 1", + "editInternalResourceDialogProxyPortMax": "Porta de proxy deve ser inferior a 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Formato de endereço IP inválido", + "editInternalResourceDialogDestinationPortMin": "Porta de destino deve ser pelo menos 1", + "editInternalResourceDialogDestinationPortMax": "Porta de destino deve ser inferior a 65536", + "createInternalResourceDialogNoSitesAvailable": "Nenhum Site Disponível", + "createInternalResourceDialogNoSitesAvailableDescription": "Você precisa ter pelo menos um site Newt com uma sub-rede configurada para criar recursos internos.", + "createInternalResourceDialogClose": "Fechar", + "createInternalResourceDialogCreateClientResource": "Criar Recurso do Cliente", + "createInternalResourceDialogCreateClientResourceDescription": "Crie um novo recurso que estará acessível aos clientes conectados ao site selecionado.", + "createInternalResourceDialogResourceProperties": "Propriedades do Recurso", + "createInternalResourceDialogName": "Nome", "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogSelectSite": "Selecionar site...", + "createInternalResourceDialogSearchSites": "Procurar sites...", + "createInternalResourceDialogNoSitesFound": "Nenhum site encontrado.", + "createInternalResourceDialogProtocol": "Protocolo", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Porta do Site", + "createInternalResourceDialogSitePortDescription": "Use esta porta para acessar o recurso no site quando conectado com um cliente.", + "createInternalResourceDialogTargetConfiguration": "Configuração do Alvo", + "createInternalResourceDialogDestinationIP": "IP de Destino", + "createInternalResourceDialogDestinationIPDescription": "O endereço IP do recurso na rede do site.", + "createInternalResourceDialogDestinationPort": "Porta de Destino", + "createInternalResourceDialogDestinationPortDescription": "A porta no IP de destino onde o recurso está acessível.", + "createInternalResourceDialogCancel": "Cancelar", + "createInternalResourceDialogCreateResource": "Criar Recurso", + "createInternalResourceDialogSuccess": "Sucesso", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno criado com sucesso", + "createInternalResourceDialogError": "Erro", + "createInternalResourceDialogFailedToCreateInternalResource": "Falha ao criar recurso interno", + "createInternalResourceDialogNameRequired": "Nome é obrigatório", + "createInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres", + "createInternalResourceDialogPleaseSelectSite": "Por favor, selecione um site", + "createInternalResourceDialogProxyPortMin": "Porta de proxy deve ser pelo menos 1", + "createInternalResourceDialogProxyPortMax": "Porta de proxy deve ser inferior a 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Formato de endereço IP inválido", + "createInternalResourceDialogDestinationPortMin": "Porta de destino deve ser pelo menos 1", + "createInternalResourceDialogDestinationPortMax": "Porta de destino deve ser inferior a 65536", + "siteConfiguration": "Configuração", + "siteAcceptClientConnections": "Aceitar Conexões de Clientes", + "siteAcceptClientConnectionsDescription": "Permitir que outros dispositivos se conectem através desta instância Newt como um gateway usando clientes.", + "siteAddress": "Endereço do Site", + "siteAddressDescription": "Especificar o endereço IP do host para que os clientes se conectem. Este é o endereço interno do site na rede Pangolin para os clientes endereçarem. Deve estar dentro da sub-rede da Organização.", + "autoLoginExternalIdp": "Login Automático com IDP Externo", + "autoLoginExternalIdpDescription": "Redirecionar imediatamente o usuário para o IDP externo para autenticação.", + "selectIdp": "Selecionar IDP", + "selectIdpPlaceholder": "Escolher um IDP...", + "selectIdpRequired": "Por favor, selecione um IDP quando o login automático estiver ativado.", + "autoLoginTitle": "Redirecionando", + "autoLoginDescription": "Redirecionando você para o provedor de identidade externo para autenticação.", + "autoLoginProcessing": "Preparando autenticação...", + "autoLoginRedirecting": "Redirecionando para login...", + "autoLoginError": "Erro de Login Automático", + "autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.", + "autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação." } From 1062e33dc8461dd83a56fbd8f1b6da2178b07c17 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:51:02 -0700 Subject: [PATCH 216/219] New translations en-us.json (Russian) --- messages/ru-RU.json | 536 ++++++++++++++++++++++---------------------- 1 file changed, 268 insertions(+), 268 deletions(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index e2cc26e2..21d94b2e 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Простейший способ создать точку входа в вашу сеть. Дополнительная настройка не требуется.", "siteWg": "Базовый WireGuard", "siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", "siteLocalDescription": "Только локальные ресурсы. Без туннелирования.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Только локальные ресурсы. Без туннелирования. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", "siteSeeAll": "Просмотреть все сайты", "siteTunnelDescription": "Выберите способ подключения к вашему сайту", "siteNewtCredentials": "Учётные данные Newt", @@ -168,7 +168,7 @@ "siteSelect": "Выберите сайт", "siteSearch": "Поиск сайта", "siteNotFound": "Сайт не найден.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Этот сайт предоставит подключение к цели.", "resourceType": "Тип ресурса", "resourceTypeDescription": "Определите, как вы хотите получать доступ к вашему ресурсу", "resourceHTTPSSettings": "Настройки HTTPS", @@ -199,7 +199,7 @@ "general": "Общие", "generalSettings": "Общие настройки", "proxy": "Прокси", - "internal": "Internal", + "internal": "Внутренний", "rules": "Правила", "resourceSettingDescription": "Настройте параметры вашего ресурса", "resourceSetting": "Настройки {resourceName}", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.", "targetTlsSubmit": "Сохранить настройки", "targets": "Конфигурация целей", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Настройте цели для маршрутизации трафика к вашим бэкэнд сервисам", "targetStickySessions": "Включить фиксированные сессии", "targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.", "methodSelect": "Выберите метод", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр", "pincodeRequirementsChars": "PIN должен содержать только цифры", "passwordRequirementsLength": "Пароль должен быть не менее 1 символа", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Требования к паролю:", + "passwordRequirementLength": "Не менее 8 символов", + "passwordRequirementUppercase": "По крайней мере, одна заглавная буква", + "passwordRequirementLowercase": "По крайней мере, одна строчная буква", + "passwordRequirementNumber": "По крайней мере, одна цифра", + "passwordRequirementSpecial": "По крайней мере, один специальный символ", + "passwordRequirementsMet": "✓ Пароль соответствует всем требованиям", + "passwordStrength": "Сила пароля", + "passwordStrengthWeak": "Слабый", + "passwordStrengthMedium": "Средний", + "passwordStrengthStrong": "Сильный", + "passwordRequirements": "Требования:", + "passwordRequirementLengthText": "8+ символов", + "passwordRequirementUppercaseText": "Заглавная буква (A-Z)", + "passwordRequirementLowercaseText": "Строчная буква (a-z)", + "passwordRequirementNumberText": "Цифра (0-9)", + "passwordRequirementSpecialText": "Специальный символ (!@#$%...)", + "passwordsDoNotMatch": "Пароли не совпадают", "otpEmailRequirementsLength": "OTP должен быть не менее 1 символа", "otpEmailSent": "OTP отправлен", "otpEmailSentDescription": "OTP был отправлен на ваш email", @@ -973,7 +973,7 @@ "logoutError": "Ошибка при выходе", "signingAs": "Вы вошли как", "serverAdmin": "Администратор сервера", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Управляемый с самовывоза", "otpEnable": "Включить Двухфакторную Аутентификацию", "otpDisable": "Отключить двухфакторную аутентификацию", "logout": "Выйти", @@ -989,9 +989,9 @@ "actionDeleteSite": "Удалить сайт", "actionGetSite": "Получить сайт", "actionListSites": "Список сайтов", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Код настройки", + "setupTokenDescription": "Введите токен настройки из консоли сервера.", + "setupTokenRequired": "Токен настройки обязателен", "actionUpdateSite": "Обновить сайт", "actionListSiteRoles": "Список разрешенных ролей сайта", "actionCreateResource": "Создать ресурс", @@ -1005,8 +1005,8 @@ "actionListAllowedResourceRoles": "Список разрешенных ролей сайта", "actionSetResourcePassword": "Задать пароль ресурса", "actionSetResourcePincode": "Установить ПИН-код ресурса", - "actionSetResourceEmailWhitelist": "Set Resource Email Whitelist", - "actionGetResourceEmailWhitelist": "Get Resource Email Whitelist", + "actionSetResourceEmailWhitelist": "Настроить белый список ресурсов email", + "actionGetResourceEmailWhitelist": "Получить белый список ресурсов email", "actionCreateTarget": "Создать цель", "actionDeleteTarget": "Удалить цель", "actionGetTarget": "Получить цель", @@ -1190,114 +1190,114 @@ "selectDomainTypeNsDescription": "Этот домен и все его субдомены. Используйте это, когда вы хотите управлять всей доменной зоной.", "selectDomainTypeCnameName": "Одиночный домен (CNAME)", "selectDomainTypeCnameDescription": "Только этот конкретный домен. Используйте это для отдельных субдоменов или отдельных записей домена.", - "selectDomainTypeWildcardName": "Wildcard Domain", + "selectDomainTypeWildcardName": "Подставной домен", "selectDomainTypeWildcardDescription": "Этот домен и его субдомены.", "domainDelegation": "Единый домен", "selectType": "Выберите тип", - "actions": "Actions", - "refresh": "Refresh", - "refreshError": "Failed to refresh data", - "verified": "Verified", - "pending": "Pending", - "sidebarBilling": "Billing", - "billing": "Billing", - "orgBillingDescription": "Manage your billing information and subscriptions", + "actions": "Действия", + "refresh": "Обновить", + "refreshError": "Не удалось обновить данные", + "verified": "Подтверждено", + "pending": "В ожидании", + "sidebarBilling": "Выставление счетов", + "billing": "Выставление счетов", + "orgBillingDescription": "Управляйте информацией о выставлении счетов и подписками", "github": "GitHub", "pangolinHosted": "Pangolin Hosted", "fossorial": "Fossorial", - "completeAccountSetup": "Complete Account Setup", - "completeAccountSetupDescription": "Set your password to get started", - "accountSetupSent": "We'll send an account setup code to this email address.", - "accountSetupCode": "Setup Code", - "accountSetupCodeDescription": "Check your email for the setup code.", - "passwordCreate": "Create Password", - "passwordCreateConfirm": "Confirm Password", - "accountSetupSubmit": "Send Setup Code", - "completeSetup": "Complete Setup", - "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", - "documentation": "Documentation", - "saveAllSettings": "Save All Settings", - "settingsUpdated": "Settings updated", - "settingsUpdatedDescription": "All settings have been updated successfully", - "settingsErrorUpdate": "Failed to update settings", - "settingsErrorUpdateDescription": "An error occurred while updating settings", - "sidebarCollapse": "Collapse", - "sidebarExpand": "Expand", - "newtUpdateAvailable": "Update Available", - "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", - "domainPickerEnterDomain": "Domain", - "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", - "domainPickerDescription": "Enter the full domain of the resource to see available options.", - "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", - "domainPickerTabAll": "All", - "domainPickerTabOrganization": "Organization", - "domainPickerTabProvided": "Provided", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Checking availability...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", - "domainPickerOrganizationDomains": "Organization Domains", - "domainPickerProvidedDomains": "Provided Domains", - "domainPickerSubdomain": "Subdomain: {subdomain}", - "domainPickerNamespace": "Namespace: {namespace}", - "domainPickerShowMore": "Show More", - "domainNotFound": "Domain Not Found", - "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", - "failed": "Failed", - "createNewOrgDescription": "Create a new organization", - "organization": "Organization", - "port": "Port", - "securityKeyManage": "Manage Security Keys", - "securityKeyDescription": "Add or remove security keys for passwordless authentication", - "securityKeyRegister": "Register New Security Key", - "securityKeyList": "Your Security Keys", - "securityKeyNone": "No security keys registered yet", - "securityKeyNameRequired": "Name is required", - "securityKeyRemove": "Remove", - "securityKeyLastUsed": "Last used: {date}", - "securityKeyNameLabel": "Security Key Name", - "securityKeyRegisterSuccess": "Security key registered successfully", - "securityKeyRegisterError": "Failed to register security key", - "securityKeyRemoveSuccess": "Security key removed successfully", - "securityKeyRemoveError": "Failed to remove security key", - "securityKeyLoadError": "Failed to load security keys", - "securityKeyLogin": "Continue with security key", - "securityKeyAuthError": "Failed to authenticate with security key", - "securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.", - "registering": "Registering...", - "securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.", - "securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.", - "securityKeyPermissionDenied": "Please allow access to your security key to continue signing in.", - "securityKeyRemovedTooQuickly": "Please keep your security key connected until the sign-in process completes.", - "securityKeyNotSupported": "Your security key may not be compatible. Please try a different security key.", - "securityKeyUnknownError": "There was a problem using your security key. Please try again.", - "twoFactorRequired": "Two-factor authentication is required to register a security key.", - "twoFactor": "Two-Factor Authentication", - "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", - "continueToApplication": "Continue to Application", - "securityKeyAdd": "Add Security Key", - "securityKeyRegisterTitle": "Register New Security Key", - "securityKeyRegisterDescription": "Connect your security key and enter a name to identify it", - "securityKeyTwoFactorRequired": "Two-Factor Authentication Required", - "securityKeyTwoFactorDescription": "Please enter your two-factor authentication code to register the security key", - "securityKeyTwoFactorRemoveDescription": "Please enter your two-factor authentication code to remove the security key", - "securityKeyTwoFactorCode": "Two-Factor Code", - "securityKeyRemoveTitle": "Remove Security Key", - "securityKeyRemoveDescription": "Enter your password to remove the security key \"{name}\"", - "securityKeyNoKeysRegistered": "No security keys registered", - "securityKeyNoKeysDescription": "Add a security key to enhance your account security", - "createDomainRequired": "Domain is required", - "createDomainAddDnsRecords": "Add DNS Records", - "createDomainAddDnsRecordsDescription": "Add the following DNS records to your domain provider to complete the setup.", - "createDomainNsRecords": "NS Records", - "createDomainRecord": "Record", - "createDomainType": "Type:", - "createDomainName": "Name:", - "createDomainValue": "Value:", - "createDomainCnameRecords": "CNAME Records", - "createDomainARecords": "A Records", - "createDomainRecordNumber": "Record {number}", - "createDomainTxtRecords": "TXT Records", + "completeAccountSetup": "Завершите настройку аккаунта", + "completeAccountSetupDescription": "Установите ваш пароль, чтобы начать", + "accountSetupSent": "Мы отправим код для настройки аккаунта на этот email адрес.", + "accountSetupCode": "Код настройки", + "accountSetupCodeDescription": "Проверьте вашу почту для получения кода настройки.", + "passwordCreate": "Создать пароль", + "passwordCreateConfirm": "Подтвердите пароль", + "accountSetupSubmit": "Отправить код настройки", + "completeSetup": "Завершить настройку", + "accountSetupSuccess": "Настройка аккаунта завершена! Добро пожаловать в Pangolin!", + "documentation": "Документация", + "saveAllSettings": "Сохранить все настройки", + "settingsUpdated": "Настройки обновлены", + "settingsUpdatedDescription": "Все настройки успешно обновлены", + "settingsErrorUpdate": "Не удалось обновить настройки", + "settingsErrorUpdateDescription": "Произошла ошибка при обновлении настроек", + "sidebarCollapse": "Свернуть", + "sidebarExpand": "Развернуть", + "newtUpdateAvailable": "Доступно обновление", + "newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.", + "domainPickerEnterDomain": "Домен", + "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, или просто myapp", + "domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.", + "domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции", + "domainPickerTabAll": "Все", + "domainPickerTabOrganization": "Организация", + "domainPickerTabProvided": "Предоставлено", + "domainPickerSortAsc": "А-Я", + "domainPickerSortDesc": "Я-А", + "domainPickerCheckingAvailability": "Проверка доступности...", + "domainPickerNoMatchingDomains": "Не найдены сопоставимые домены. Попробуйте другой домен или проверьте настройки доменов вашей организации.", + "domainPickerOrganizationDomains": "Домены организации", + "domainPickerProvidedDomains": "Предоставленные домены", + "domainPickerSubdomain": "Поддомен: {subdomain}", + "domainPickerNamespace": "Пространство имен: {namespace}", + "domainPickerShowMore": "Показать еще", + "domainNotFound": "Домен не найден", + "domainNotFoundDescription": "Этот ресурс отключен, так как домен больше не существует в нашей системе. Пожалуйста, установите новый домен для этого ресурса.", + "failed": "Ошибка", + "createNewOrgDescription": "Создать новую организацию", + "organization": "Организация", + "port": "Порт", + "securityKeyManage": "Управление ключами безопасности", + "securityKeyDescription": "Добавить или удалить ключи безопасности для аутентификации без пароля", + "securityKeyRegister": "Зарегистрировать новый ключ безопасности", + "securityKeyList": "Ваши ключи безопасности", + "securityKeyNone": "Ключи безопасности еще не зарегистрированы", + "securityKeyNameRequired": "Имя обязательно", + "securityKeyRemove": "Удалить", + "securityKeyLastUsed": "Последнее использование: {date}", + "securityKeyNameLabel": "Имя ключа безопасности", + "securityKeyRegisterSuccess": "Ключ безопасности успешно зарегистрирован", + "securityKeyRegisterError": "Не удалось зарегистрировать ключ безопасности", + "securityKeyRemoveSuccess": "Ключ безопасности успешно удален", + "securityKeyRemoveError": "Не удалось удалить ключ безопасности", + "securityKeyLoadError": "Не удалось загрузить ключи безопасности", + "securityKeyLogin": "Продолжить с ключом безопасности", + "securityKeyAuthError": "Не удалось аутентифицироваться с ключом безопасности", + "securityKeyRecommendation": "Зарегистрируйте резервный ключ безопасности на другом устройстве, чтобы всегда иметь доступ к вашему аккаунту.", + "registering": "Регистрация...", + "securityKeyPrompt": "Пожалуйста, подтвердите свою личность с использованием вашего ключа безопасности. Убедитесь, что ваш ключ безопасности подключен и готов.", + "securityKeyBrowserNotSupported": "Ваш браузер не поддерживает ключи безопасности. Пожалуйста, используйте современный браузер, такой как Chrome, Firefox или Safari.", + "securityKeyPermissionDenied": "Пожалуйста, разрешите доступ к вашему ключу безопасности, чтобы продолжить вход.", + "securityKeyRemovedTooQuickly": "Пожалуйста, держите ваш ключ безопасности подключенным, пока процесс входа не завершится.", + "securityKeyNotSupported": "Ваш ключ безопасности может быть несовместим. Попробуйте другой ключ безопасности.", + "securityKeyUnknownError": "Произошла проблема при использовании вашего ключа безопасности. Пожалуйста, попробуйте еще раз.", + "twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.", + "twoFactor": "Двухфакторная аутентификация", + "adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.", + "continueToApplication": "Перейти к приложению", + "securityKeyAdd": "Добавить ключ безопасности", + "securityKeyRegisterTitle": "Регистрация нового ключа безопасности", + "securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации", + "securityKeyTwoFactorRequired": "Требуется двухфакторная аутентификация", + "securityKeyTwoFactorDescription": "Пожалуйста, введите ваш код двухфакторной аутентификации для регистрации ключа безопасности", + "securityKeyTwoFactorRemoveDescription": "Пожалуйста, введите ваш код двухфакторной аутентификации для удаления ключа безопасности", + "securityKeyTwoFactorCode": "Код двухфакторной аутентификации", + "securityKeyRemoveTitle": "Удалить ключ безопасности", + "securityKeyRemoveDescription": "Введите ваш пароль для удаления ключа безопасности \"{name}\"", + "securityKeyNoKeysRegistered": "Ключи безопасности не зарегистрированы", + "securityKeyNoKeysDescription": "Добавьте ключ безопасности, чтобы повысить безопасность вашего аккаунта", + "createDomainRequired": "Домен обязателен", + "createDomainAddDnsRecords": "Добавить DNS записи", + "createDomainAddDnsRecordsDescription": "Добавьте следующие DNS записи у вашего провайдера доменных имен для завершения настройки.", + "createDomainNsRecords": "NS Записи", + "createDomainRecord": "Запись", + "createDomainType": "Тип:", + "createDomainName": "Имя:", + "createDomainValue": "Значение:", + "createDomainCnameRecords": "CNAME Записи", + "createDomainARecords": "A Записи", + "createDomainRecordNumber": "Запись {number}", + "createDomainTxtRecords": "TXT Записи", "createDomainSaveTheseRecords": "Сохранить эти записи", "createDomainSaveTheseRecordsDescription": "Обязательно сохраните эти DNS записи, так как вы их больше не увидите.", "createDomainDnsPropagation": "Распространение DNS", @@ -1311,144 +1311,144 @@ "privacyPolicy": "политика конфиденциальности" }, "siteRequired": "Необходимо указать сайт.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", + "olmTunnel": "Olm Туннель", + "olmTunnelDescription": "Используйте Olm для подключений клиентов", + "errorCreatingClient": "Ошибка при создании клиента", + "clientDefaultsNotFound": "Настройки клиента по умолчанию не найдены", + "createClient": "Создать клиента", + "createClientDescription": "Создайте нового клиента для подключения к вашим сайтам", + "seeAllClients": "Просмотреть всех клиентов", + "clientInformation": "Информация о клиенте", + "clientNamePlaceholder": "Имя клиента", + "address": "Адрес", + "subnetPlaceholder": "Подсеть", + "addressDescription": "Адрес, который этот клиент будет использовать для подключения", + "selectSites": "Выберите сайты", + "sitesDescription": "Клиент будет иметь подключение к выбранным сайтам", + "clientInstallOlm": "Установить Olm", + "clientInstallOlmDescription": "Запустите Olm на вашей системе", + "clientOlmCredentials": "Учётные данные Olm", + "clientOlmCredentialsDescription": "Так Olm будет аутентифицироваться через сервер", + "olmEndpoint": "Конечная точка Olm", "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "olmSecretKey": "Секретный ключ Olm", + "clientCredentialsSave": "Сохраните ваши учётные данные", + "clientCredentialsSaveDescription": "Вы сможете увидеть их только один раз. Обязательно скопируйте в безопасное место.", + "generalSettingsDescription": "Настройте общие параметры для этого клиента", + "clientUpdated": "Клиент обновлен", + "clientUpdatedDescription": "Клиент был обновлён.", + "clientUpdateFailed": "Не удалось обновить клиента", + "clientUpdateError": "Произошла ошибка при обновлении клиента.", + "sitesFetchFailed": "Не удалось получить сайты", + "sitesFetchError": "Произошла ошибка при получении сайтов.", + "olmErrorFetchReleases": "Произошла ошибка при получении релизов Olm.", + "olmErrorFetchLatest": "Произошла ошибка при получении последнего релиза Olm.", + "remoteSubnets": "Удалённые подсети", + "enterCidrRange": "Введите диапазон CIDR", + "remoteSubnetsDescription": "Добавьте диапазоны адресов CIDR, которые можно получить из этого сайта удаленно, используя клиентов. Используйте формат 10.0.0.0/24. Это относится ТОЛЬКО к подключению через VPN клиентов.", + "resourceEnableProxy": "Включить публичный прокси", + "resourceEnableProxyDescription": "Включите публичное проксирование для этого ресурса. Это позволяет получить доступ к ресурсу извне сети через облако через открытый порт. Требуется конфигурация Traefik.", + "externalProxyEnabled": "Внешний прокси включен", + "addNewTarget": "Добавить новую цель", + "targetsList": "Список целей", + "targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель", + "httpMethod": "HTTP метод", + "selectHttpMethod": "Выберите HTTP метод", + "domainPickerSubdomainLabel": "Поддомен", + "domainPickerBaseDomainLabel": "Основной домен", + "domainPickerSearchDomains": "Поиск доменов...", + "domainPickerNoDomainsFound": "Доменов не найдено", + "domainPickerLoadingDomains": "Загрузка доменов...", + "domainPickerSelectBaseDomain": "Выбор основного домена...", + "domainPickerNotAvailableForCname": "Не доступно для CNAME доменов", + "domainPickerEnterSubdomainOrLeaveBlank": "Введите поддомен или оставьте пустым для использования основного домена.", + "domainPickerEnterSubdomainToSearch": "Введите поддомен для поиска и выбора из доступных свободных доменов.", + "domainPickerFreeDomains": "Свободные домены", + "domainPickerSearchForAvailableDomains": "Поиск доступных доменов", + "resourceDomain": "Домен", + "resourceEditDomain": "Редактировать домен", + "siteName": "Имя сайта", + "proxyPort": "Порт", + "resourcesTableProxyResources": "Проксированные ресурсы", + "resourcesTableClientResources": "Клиентские ресурсы", + "resourcesTableNoProxyResourcesFound": "Проксированных ресурсов не найдено.", + "resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.", + "resourcesTableDestination": "Пункт назначения", + "resourcesTableTheseResourcesForUseWith": "Эти ресурсы предназначены для использования с", + "resourcesTableClients": "Клиенты", + "resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.", + "editInternalResourceDialogEditClientResource": "Редактировать ресурс клиента", + "editInternalResourceDialogUpdateResourceProperties": "Обновите свойства ресурса и настройку цели для {resourceName}.", + "editInternalResourceDialogResourceProperties": "Свойства ресурса", + "editInternalResourceDialogName": "Имя", + "editInternalResourceDialogProtocol": "Протокол", + "editInternalResourceDialogSitePort": "Порт сайта", + "editInternalResourceDialogTargetConfiguration": "Настройка цели", + "editInternalResourceDialogDestinationIP": "Целевая IP", + "editInternalResourceDialogDestinationPort": "Целевой порт", + "editInternalResourceDialogCancel": "Отмена", + "editInternalResourceDialogSaveResource": "Сохранить ресурс", + "editInternalResourceDialogSuccess": "Успешно", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Внутренний ресурс успешно обновлен", + "editInternalResourceDialogError": "Ошибка", + "editInternalResourceDialogFailedToUpdateInternalResource": "Не удалось обновить внутренний ресурс", + "editInternalResourceDialogNameRequired": "Имя обязательно", + "editInternalResourceDialogNameMaxLength": "Имя не должно быть длиннее 255 символов", + "editInternalResourceDialogProxyPortMin": "Порт прокси должен быть не менее 1", + "editInternalResourceDialogProxyPortMax": "Порт прокси должен быть меньше 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Неверный формат IP адреса", + "editInternalResourceDialogDestinationPortMin": "Целевой порт должен быть не менее 1", + "editInternalResourceDialogDestinationPortMax": "Целевой порт должен быть меньше 65536", + "createInternalResourceDialogNoSitesAvailable": "Нет доступных сайтов", + "createInternalResourceDialogNoSitesAvailableDescription": "Вам необходимо иметь хотя бы один сайт Newt с настроенной подсетью для создания внутреннего ресурса.", + "createInternalResourceDialogClose": "Закрыть", + "createInternalResourceDialogCreateClientResource": "Создать ресурс клиента", + "createInternalResourceDialogCreateClientResourceDescription": "Создайте новый ресурс, который будет доступен клиентам, подключенным к выбранному сайту.", + "createInternalResourceDialogResourceProperties": "Свойства ресурса", + "createInternalResourceDialogName": "Имя", + "createInternalResourceDialogSite": "Сайт", + "createInternalResourceDialogSelectSite": "Выберите сайт...", + "createInternalResourceDialogSearchSites": "Поиск сайтов...", + "createInternalResourceDialogNoSitesFound": "Сайты не найдены.", + "createInternalResourceDialogProtocol": "Протокол", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Порт сайта", + "createInternalResourceDialogSitePortDescription": "Используйте этот порт для доступа к ресурсу на сайте при подключении с клиентом.", + "createInternalResourceDialogTargetConfiguration": "Настройка цели", + "createInternalResourceDialogDestinationIP": "Целевая IP", + "createInternalResourceDialogDestinationIPDescription": "IP-адрес ресурса в сети сайта.", + "createInternalResourceDialogDestinationPort": "Целевой порт", + "createInternalResourceDialogDestinationPortDescription": "Порт на IP-адресе назначения, где доступен ресурс.", + "createInternalResourceDialogCancel": "Отмена", + "createInternalResourceDialogCreateResource": "Создать ресурс", + "createInternalResourceDialogSuccess": "Успешно", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Внутренний ресурс успешно создан", + "createInternalResourceDialogError": "Ошибка", + "createInternalResourceDialogFailedToCreateInternalResource": "Не удалось создать внутренний ресурс", + "createInternalResourceDialogNameRequired": "Имя обязательно", + "createInternalResourceDialogNameMaxLength": "Имя должно содержать менее 255 символов", + "createInternalResourceDialogPleaseSelectSite": "Пожалуйста, выберите сайт", + "createInternalResourceDialogProxyPortMin": "Прокси-порт должен быть не менее 1", + "createInternalResourceDialogProxyPortMax": "Прокси-порт должен быть меньше 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Неверный формат IP-адреса", + "createInternalResourceDialogDestinationPortMin": "Целевой порт должен быть не менее 1", + "createInternalResourceDialogDestinationPortMax": "Целевой порт должен быть меньше 65536", + "siteConfiguration": "Конфигурация", + "siteAcceptClientConnections": "Принимать подключения клиентов", + "siteAcceptClientConnectionsDescription": "Разрешите другим устройствам подключаться через этот экземпляр Newt в качестве шлюза с использованием клиентов.", + "siteAddress": "Адрес сайта", + "siteAddressDescription": "Укажите IP-адрес хоста для подключения клиентов. Это внутренний адрес сайта в сети Pangolin для адресации клиентов. Должен находиться в пределах подсети организационного уровня.", + "autoLoginExternalIdp": "Автоматический вход с внешним провайдером", + "autoLoginExternalIdpDescription": "Немедленно перенаправьте пользователя к внешнему провайдеру для аутентификации.", + "selectIdp": "Выберите провайдера", + "selectIdpPlaceholder": "Выберите провайдера...", + "selectIdpRequired": "Пожалуйста, выберите провайдера, когда автоматический вход включен.", + "autoLoginTitle": "Перенаправление", + "autoLoginDescription": "Перенаправление вас к внешнему провайдеру для аутентификации.", + "autoLoginProcessing": "Подготовка аутентификации...", + "autoLoginRedirecting": "Перенаправление к входу...", + "autoLoginError": "Ошибка автоматического входа", + "autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.", + "autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации." } From b4a57e630c209a5f78248f371f1b0e3366a6c103 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:51:03 -0700 Subject: [PATCH 217/219] New translations en-us.json (Turkish) --- messages/tr-TR.json | 254 ++++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 127 deletions(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 6ab3d7ba..f1e3b793 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.", "siteWg": "Temel WireGuard", "siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR", "siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR", "siteSeeAll": "Tüm Siteleri Gör", "siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin", "siteNewtCredentials": "Newt Kimlik Bilgileri", @@ -168,7 +168,7 @@ "siteSelect": "Site seç", "siteSearch": "Site ara", "siteNotFound": "Herhangi bir site bulunamadı.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Bu site hedefe bağlantı sağlayacaktır.", "resourceType": "Kaynak Türü", "resourceTypeDescription": "Kaynağınıza nasıl erişmek istediğinizi belirleyin", "resourceHTTPSSettings": "HTTPS Ayarları", @@ -199,7 +199,7 @@ "general": "Genel", "generalSettings": "Genel Ayarlar", "proxy": "Vekil Sunucu", - "internal": "Internal", + "internal": "Dahili", "rules": "Kurallar", "resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın", "resourceSetting": "{resourceName} Ayarları", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'", "targetTlsSubmit": "Ayarları Kaydet", "targets": "Hedefler Konfigürasyonu", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Trafiği arka uç hizmetlerinize yönlendirmek için hedefleri ayarlayın", "targetStickySessions": "Yapışkan Oturumları Etkinleştir", "targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.", "methodSelect": "Yöntemi Seç", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır", "pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır", "passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "Şifre gereksinimleri:", + "passwordRequirementLength": "En az 8 karakter uzunluğunda", + "passwordRequirementUppercase": "En az bir büyük harf", + "passwordRequirementLowercase": "En az bir küçük harf", + "passwordRequirementNumber": "En az bir sayı", + "passwordRequirementSpecial": "En az bir özel karakter", + "passwordRequirementsMet": "✓ Şifre tüm gereksinimleri karşılıyor", + "passwordStrength": "Şifre gücü", + "passwordStrengthWeak": "Zayıf", + "passwordStrengthMedium": "Orta", + "passwordStrengthStrong": "Güçlü", + "passwordRequirements": "Gereksinimler:", + "passwordRequirementLengthText": "8+ karakter", + "passwordRequirementUppercaseText": "Büyük harf (A-Z)", + "passwordRequirementLowercaseText": "Küçük harf (a-z)", + "passwordRequirementNumberText": "Sayı (0-9)", + "passwordRequirementSpecialText": "Özel karakter (!@#$%...)", + "passwordsDoNotMatch": "Parolalar eşleşmiyor", "otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır", "otpEmailSent": "OTP Gönderildi", "otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi", @@ -973,7 +973,7 @@ "logoutError": "Çıkış yaparken hata", "signingAs": "Olarak giriş yapıldı", "serverAdmin": "Sunucu Yöneticisi", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Yönetilen Self-Hosted", "otpEnable": "İki faktörlü özelliğini etkinleştir", "otpDisable": "İki faktörlü özelliğini devre dışı bırak", "logout": "Çıkış Yap", @@ -989,9 +989,9 @@ "actionDeleteSite": "Siteyi Sil", "actionGetSite": "Siteyi Al", "actionListSites": "Siteleri Listele", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Kurulum Simgesi", + "setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.", + "setupTokenRequired": "Kurulum simgesi gerekli", "actionUpdateSite": "Siteyi Güncelle", "actionListSiteRoles": "İzin Verilen Site Rolleri Listele", "actionCreateResource": "Kaynak Oluştur", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.", "remoteSubnets": "Uzak Alt Ağlar", "enterCidrRange": "CIDR aralığını girin", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "Bu siteye uzaktan erişilebilen CIDR aralıklarını ekleyin. 10.0.0.0/24 formatını kullanın. Bu YALNIZCA VPN istemci bağlantıları için geçerlidir.", "resourceEnableProxy": "Genel Proxy'i Etkinleştir", "resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.", "externalProxyEnabled": "Dış Proxy Etkinleştirildi", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", + "addNewTarget": "Yeni Hedef Ekle", + "targetsList": "Hedefler Listesi", + "targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu", + "httpMethod": "HTTP Yöntemi", + "selectHttpMethod": "HTTP yöntemini seçin", + "domainPickerSubdomainLabel": "Alt Alan Adı", + "domainPickerBaseDomainLabel": "Temel Alan Adı", + "domainPickerSearchDomains": "Alan adlarını ara...", + "domainPickerNoDomainsFound": "Hiçbir alan adı bulunamadı", + "domainPickerLoadingDomains": "Alan adları yükleniyor...", + "domainPickerSelectBaseDomain": "Temel alan adını seçin...", + "domainPickerNotAvailableForCname": "CNAME alan adları için kullanılabilir değil", + "domainPickerEnterSubdomainOrLeaveBlank": "Alt alan adını girin veya temel alan adını kullanmak için boş bırakın.", + "domainPickerEnterSubdomainToSearch": "Mevcut ücretsiz alan adları arasından aramak ve seçmek için bir alt alan adı girin.", + "domainPickerFreeDomains": "Ücretsiz Alan Adları", + "domainPickerSearchForAvailableDomains": "Mevcut alan adlarını ara", + "resourceDomain": "Alan Adı", + "resourceEditDomain": "Alan Adını Düzenle", + "siteName": "Site Adı", + "proxyPort": "Bağlantı Noktası", + "resourcesTableProxyResources": "Proxy Kaynaklar", + "resourcesTableClientResources": "İstemci Kaynaklar", + "resourcesTableNoProxyResourcesFound": "Hiçbir proxy kaynağı bulunamadı.", + "resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.", + "resourcesTableDestination": "Hedef", + "resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için", + "resourcesTableClients": "İstemciler", + "resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.", + "editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin", + "editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.", + "editInternalResourceDialogResourceProperties": "Kaynak Özellikleri", + "editInternalResourceDialogName": "Ad", + "editInternalResourceDialogProtocol": "Protokol", + "editInternalResourceDialogSitePort": "Site Bağlantı Noktası", + "editInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma", + "editInternalResourceDialogDestinationIP": "Hedef IP", + "editInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası", + "editInternalResourceDialogCancel": "İptal", + "editInternalResourceDialogSaveResource": "Kaynağı Kaydet", + "editInternalResourceDialogSuccess": "Başarı", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Dahili kaynak başarıyla güncellendi", + "editInternalResourceDialogError": "Hata", + "editInternalResourceDialogFailedToUpdateInternalResource": "Dahili kaynak güncellenemedi", + "editInternalResourceDialogNameRequired": "Ad gerekli", + "editInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır", + "editInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır", + "editInternalResourceDialogProxyPortMax": "Proxy bağlantı noktası 65536'dan küçük olmalıdır", + "editInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı", + "editInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır", + "editInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır", + "createInternalResourceDialogNoSitesAvailable": "Site Bulunamadı", + "createInternalResourceDialogNoSitesAvailableDescription": "Dahili kaynak oluşturmak için en az bir Newt sitesine ve alt ağa sahip olmalısınız.", + "createInternalResourceDialogClose": "Kapat", + "createInternalResourceDialogCreateClientResource": "İstemci Kaynağı Oluştur", + "createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemciler için erişilebilir olacak yeni bir kaynak oluşturun.", + "createInternalResourceDialogResourceProperties": "Kaynak Özellikleri", + "createInternalResourceDialogName": "Ad", "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogSelectSite": "Site seç...", + "createInternalResourceDialogSearchSites": "Siteleri ara...", + "createInternalResourceDialogNoSitesFound": "Site bulunamadı.", + "createInternalResourceDialogProtocol": "Protokol", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Site Bağlantı Noktası", + "createInternalResourceDialogSitePortDescription": "İstemci ile bağlanıldığında site üzerindeki kaynağa erişmek için bu bağlantı noktasını kullanın.", + "createInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma", + "createInternalResourceDialogDestinationIP": "Hedef IP", + "createInternalResourceDialogDestinationIPDescription": "Site ağındaki kaynağın IP adresi.", + "createInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası", + "createInternalResourceDialogDestinationPortDescription": "Kaynağa erişilebilecek hedef IP üzerindeki bağlantı noktası.", + "createInternalResourceDialogCancel": "İptal", + "createInternalResourceDialogCreateResource": "Kaynak Oluştur", + "createInternalResourceDialogSuccess": "Başarı", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Dahili kaynak başarıyla oluşturuldu", + "createInternalResourceDialogError": "Hata", + "createInternalResourceDialogFailedToCreateInternalResource": "Dahili kaynak oluşturulamadı", + "createInternalResourceDialogNameRequired": "Ad gerekli", + "createInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır", + "createInternalResourceDialogPleaseSelectSite": "Lütfen bir site seçin", + "createInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır", + "createInternalResourceDialogProxyPortMax": "Proxy bağlantı noktası 65536'dan küçük olmalıdır", + "createInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı", + "createInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır", + "createInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır", + "siteConfiguration": "Yapılandırma", + "siteAcceptClientConnections": "İstemci Bağlantılarını Kabul Et", + "siteAcceptClientConnectionsDescription": "Bu Newt örneğini bir geçit olarak kullanarak diğer cihazların bağlanmasına izin verin.", + "siteAddress": "Site Adresi", + "siteAddressDescription": "İstemcilerin bağlanması için hostun IP adresini belirtin. Bu, Pangolin ağındaki sitenin iç adresidir ve istemciler için atlas olmalıdır. Org alt ağına düşmelidir.", + "autoLoginExternalIdp": "Harici IDP ile Otomatik Giriş", + "autoLoginExternalIdpDescription": "Kullanıcıyı kimlik doğrulama için otomatik olarak harici IDP'ye yönlendirin.", + "selectIdp": "IDP Seç", + "selectIdpPlaceholder": "IDP seçin...", + "selectIdpRequired": "Otomatik giriş etkinleştirildiğinde lütfen bir IDP seçin.", + "autoLoginTitle": "Yönlendiriliyor", + "autoLoginDescription": "Kimlik doğrulama için harici kimlik sağlayıcıya yönlendiriliyorsunuz.", + "autoLoginProcessing": "Kimlik doğrulama hazırlanıyor...", + "autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...", + "autoLoginError": "Otomatik Giriş Hatası", + "autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.", + "autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı." } From e9d9d6e2f4eb43ad20171f03df917670d0607d50 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:51:04 -0700 Subject: [PATCH 218/219] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 256 ++++++++++++++++++++++---------------------- 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index c42826ca..d7f27b72 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "最简单的方式来连接到您的网络。不需要任何额外设置。", "siteWg": "基本 WireGuard", "siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。", "siteLocalDescription": "仅限本地资源。不需要隧道。", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。", "siteSeeAll": "查看所有站点", "siteTunnelDescription": "确定如何连接到您的网站", "siteNewtCredentials": "Newt 凭据", @@ -168,7 +168,7 @@ "siteSelect": "选择站点", "siteSearch": "搜索站点", "siteNotFound": "未找到站点。", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "此站点将为目标提供连接。", "resourceType": "资源类型", "resourceTypeDescription": "确定如何访问您的资源", "resourceHTTPSSettings": "HTTPS 设置", @@ -199,7 +199,7 @@ "general": "概览", "generalSettings": "常规设置", "proxy": "代理服务器", - "internal": "Internal", + "internal": "内部设置", "rules": "规则", "resourceSettingDescription": "配置您资源上的设置", "resourceSetting": "{resourceName} 设置", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。", "targetTlsSubmit": "保存设置", "targets": "目标配置", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "设置目标来路由流量到您的后端服务", "targetStickySessions": "启用置顶会话", "targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。", "methodSelect": "选择方法", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "PIN码必须是6位数字", "pincodeRequirementsChars": "PIN 必须只包含数字", "passwordRequirementsLength": "密码必须至少 1 个字符长", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordRequirementsTitle": "密码要求:", + "passwordRequirementLength": "至少8个字符长", + "passwordRequirementUppercase": "至少一个大写字母", + "passwordRequirementLowercase": "至少一个小写字母", + "passwordRequirementNumber": "至少一个数字", + "passwordRequirementSpecial": "至少一个特殊字符", + "passwordRequirementsMet": "✓ 密码满足所有要求", + "passwordStrength": "密码强度", + "passwordStrengthWeak": "弱", + "passwordStrengthMedium": "中", + "passwordStrengthStrong": "强", + "passwordRequirements": "要求:", + "passwordRequirementLengthText": "8+ 个字符", + "passwordRequirementUppercaseText": "大写字母 (A-Z)", + "passwordRequirementLowercaseText": "小写字母 (a-z)", + "passwordRequirementNumberText": "数字 (0-9)", + "passwordRequirementSpecialText": "特殊字符 (!@#$%...)", + "passwordsDoNotMatch": "密码不匹配", "otpEmailRequirementsLength": "OTP 必须至少 1 个字符长", "otpEmailSent": "OTP 已发送", "otpEmailSentDescription": "OTP 已经发送到您的电子邮件", @@ -973,7 +973,7 @@ "logoutError": "注销错误", "signingAs": "登录为", "serverAdmin": "服务器管理员", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "托管自托管", "otpEnable": "启用双因子认证", "otpDisable": "禁用双因子认证", "logout": "登出", @@ -989,9 +989,9 @@ "actionDeleteSite": "删除站点", "actionGetSite": "获取站点", "actionListSites": "站点列表", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "设置令牌", + "setupTokenDescription": "从服务器控制台输入设置令牌。", + "setupTokenRequired": "需要设置令牌", "actionUpdateSite": "更新站点", "actionListSiteRoles": "允许站点角色列表", "actionCreateResource": "创建资源", @@ -1345,110 +1345,110 @@ "olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。", "remoteSubnets": "远程子网", "enterCidrRange": "输入 CIDR 范围", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", + "remoteSubnetsDescription": "添加可以通过客户端远程访问该站点的CIDR范围。使用类似10.0.0.0/24的格式。这仅适用于VPN客户端连接。", "resourceEnableProxy": "启用公共代理", "resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。", "externalProxyEnabled": "外部代理已启用", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "addNewTarget": "添加新目标", + "targetsList": "目标列表", + "targetErrorDuplicateTargetFound": "找到重复的目标", + "httpMethod": "HTTP 方法", + "selectHttpMethod": "选择 HTTP 方法", + "domainPickerSubdomainLabel": "子域名", + "domainPickerBaseDomainLabel": "根域名", + "domainPickerSearchDomains": "搜索域名...", + "domainPickerNoDomainsFound": "未找到域名", + "domainPickerLoadingDomains": "加载域名...", + "domainPickerSelectBaseDomain": "选择根域名...", + "domainPickerNotAvailableForCname": "不适用于CNAME域", + "domainPickerEnterSubdomainOrLeaveBlank": "输入子域名或留空以使用根域名。", + "domainPickerEnterSubdomainToSearch": "输入一个子域名以搜索并从可用免费域名中选择。", + "domainPickerFreeDomains": "免费域名", + "domainPickerSearchForAvailableDomains": "搜索可用域名", + "resourceDomain": "域名", + "resourceEditDomain": "编辑域名", + "siteName": "站点名称", + "proxyPort": "端口", + "resourcesTableProxyResources": "代理资源", + "resourcesTableClientResources": "客户端资源", + "resourcesTableNoProxyResourcesFound": "未找到代理资源。", + "resourcesTableNoInternalResourcesFound": "未找到内部资源。", + "resourcesTableDestination": "目标", + "resourcesTableTheseResourcesForUseWith": "这些资源供...使用", + "resourcesTableClients": "客户端", + "resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。", + "editInternalResourceDialogEditClientResource": "编辑客户端资源", + "editInternalResourceDialogUpdateResourceProperties": "更新{resourceName}的资源属性和目标配置。", + "editInternalResourceDialogResourceProperties": "资源属性", + "editInternalResourceDialogName": "名称", + "editInternalResourceDialogProtocol": "协议", + "editInternalResourceDialogSitePort": "站点端口", + "editInternalResourceDialogTargetConfiguration": "目标配置", + "editInternalResourceDialogDestinationIP": "目标IP", + "editInternalResourceDialogDestinationPort": "目标端口", + "editInternalResourceDialogCancel": "取消", + "editInternalResourceDialogSaveResource": "保存资源", + "editInternalResourceDialogSuccess": "成功", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "内部资源更新成功", + "editInternalResourceDialogError": "错误", + "editInternalResourceDialogFailedToUpdateInternalResource": "更新内部资源失败", + "editInternalResourceDialogNameRequired": "名称为必填项", + "editInternalResourceDialogNameMaxLength": "名称长度必须小于255个字符", + "editInternalResourceDialogProxyPortMin": "代理端口必须至少为1", + "editInternalResourceDialogProxyPortMax": "代理端口必须小于65536", + "editInternalResourceDialogInvalidIPAddressFormat": "无效的IP地址格式", + "editInternalResourceDialogDestinationPortMin": "目标端口必须至少为1", + "editInternalResourceDialogDestinationPortMax": "目标端口必须小于65536", + "createInternalResourceDialogNoSitesAvailable": "暂无可用站点", + "createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一个子网的Newt站点来创建内部资源。", + "createInternalResourceDialogClose": "关闭", + "createInternalResourceDialogCreateClientResource": "创建客户端资源", + "createInternalResourceDialogCreateClientResourceDescription": "创建一个新资源,该资源将可供连接到所选站点的客户端访问。", + "createInternalResourceDialogResourceProperties": "资源属性", + "createInternalResourceDialogName": "名称", + "createInternalResourceDialogSite": "站点", + "createInternalResourceDialogSelectSite": "选择站点...", + "createInternalResourceDialogSearchSites": "搜索站点...", + "createInternalResourceDialogNoSitesFound": "未找到站点。", + "createInternalResourceDialogProtocol": "协议", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "站点端口", + "createInternalResourceDialogSitePortDescription": "使用此端口在连接到客户端时访问站点上的资源。", + "createInternalResourceDialogTargetConfiguration": "目标配置", + "createInternalResourceDialogDestinationIP": "目标IP", + "createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP地址。", + "createInternalResourceDialogDestinationPort": "目标端口", + "createInternalResourceDialogDestinationPortDescription": "资源在目标IP上可访问的端口。", + "createInternalResourceDialogCancel": "取消", + "createInternalResourceDialogCreateResource": "创建资源", + "createInternalResourceDialogSuccess": "成功", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "内部资源创建成功", + "createInternalResourceDialogError": "错误", + "createInternalResourceDialogFailedToCreateInternalResource": "创建内部资源失败", + "createInternalResourceDialogNameRequired": "名称为必填项", + "createInternalResourceDialogNameMaxLength": "名称长度必须小于255个字符", + "createInternalResourceDialogPleaseSelectSite": "请选择一个站点", + "createInternalResourceDialogProxyPortMin": "代理端口必须至少为1", + "createInternalResourceDialogProxyPortMax": "代理端口必须小于65536", + "createInternalResourceDialogInvalidIPAddressFormat": "无效的IP地址格式", + "createInternalResourceDialogDestinationPortMin": "目标端口必须至少为1", + "createInternalResourceDialogDestinationPortMax": "目标端口必须小于65536", + "siteConfiguration": "配置", + "siteAcceptClientConnections": "接受客户端连接", + "siteAcceptClientConnectionsDescription": "允许其他设备通过此Newt实例使用客户端作为网关连接。", + "siteAddress": "站点地址", + "siteAddressDescription": "指定主机的IP地址以供客户端连接。这是Pangolin网络中站点的内部地址,供客户端访问。必须在Org子网内。", + "autoLoginExternalIdp": "自动使用外部IDP登录", + "autoLoginExternalIdpDescription": "立即将用户重定向到外部IDP进行身份验证。", + "selectIdp": "选择IDP", + "selectIdpPlaceholder": "选择一个IDP...", + "selectIdpRequired": "在启用自动登录时,请选择一个IDP。", + "autoLoginTitle": "重定向中", + "autoLoginDescription": "正在将您重定向到外部身份提供商进行身份验证。", + "autoLoginProcessing": "准备身份验证...", + "autoLoginRedirecting": "重定向到登录...", + "autoLoginError": "自动登录错误", + "autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。", + "autoLoginErrorGeneratingUrl": "生成身份验证URL失败。" } From 6e5f429e0a3af1c59f4f830a46c5d46f745d91fd Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sat, 23 Aug 2025 15:51:05 -0700 Subject: [PATCH 219/219] New translations en-us.json (Norwegian Bokmal) --- messages/nb-NO.json | 250 ++++++++++++++++++++++---------------------- 1 file changed, 125 insertions(+), 125 deletions(-) diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 02ef58b9..3d2467ce 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.", "siteWg": "Grunnleggende WireGuard", "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett er nødvendig. FUNGERER KUN PÅ SELVHOSTEDE NODER", "siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Kun lokale ressurser. Ingen tunneling. FUNGERER KUN PÅ SELVHOSTEDE NODER", "siteSeeAll": "Se alle områder", "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", "siteNewtCredentials": "Newt påloggingsinformasjon", @@ -168,7 +168,7 @@ "siteSelect": "Velg område", "siteSearch": "Søk i område", "siteNotFound": "Ingen område funnet.", - "siteSelectionDescription": "This site will provide connectivity to the target.", + "siteSelectionDescription": "Dette området vil gi tilkobling til mål.", "resourceType": "Ressurstype", "resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din", "resourceHTTPSSettings": "HTTPS-innstillinger", @@ -199,7 +199,7 @@ "general": "Generelt", "generalSettings": "Generelle innstillinger", "proxy": "Proxy", - "internal": "Internal", + "internal": "Intern", "rules": "Regler", "resourceSettingDescription": "Konfigurer innstillingene på ressursen din", "resourceSetting": "{resourceName} Innstillinger", @@ -493,7 +493,7 @@ "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", "targetTlsSubmit": "Lagre innstillinger", "targets": "Målkonfigurasjon", - "targetsDescription": "Set up targets to route traffic to your backend services", + "targetsDescription": "Sett opp mål for å rute trafikk til dine backend-tjenester", "targetStickySessions": "Aktiver klebrige sesjoner", "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", "methodSelect": "Velg metode", @@ -836,24 +836,24 @@ "pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer", "pincodeRequirementsChars": "PIN må kun inneholde tall", "passwordRequirementsLength": "Passord må være minst 1 tegn langt", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", + "passwordRequirementsTitle": "Passordkrav:", + "passwordRequirementLength": "Minst 8 tegn lang", + "passwordRequirementUppercase": "Minst én stor bokstav", + "passwordRequirementLowercase": "Minst én liten bokstav", + "passwordRequirementNumber": "Minst ét tall", + "passwordRequirementSpecial": "Minst ett spesialtegn", + "passwordRequirementsMet": "✓ Passord oppfyller alle krav", + "passwordStrength": "Passordstyrke", + "passwordStrengthWeak": "Svakt", "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", + "passwordStrengthStrong": "Sterkt", + "passwordRequirements": "Krav:", + "passwordRequirementLengthText": "8+ tegn", + "passwordRequirementUppercaseText": "Stor bokstav (A-Z)", + "passwordRequirementLowercaseText": "Liten bokstav (a-z)", + "passwordRequirementNumberText": "Tall (0-9)", + "passwordRequirementSpecialText": "Spesialtegn (!@#$%...)", + "passwordsDoNotMatch": "Passordene stemmer ikke", "otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.", "otpEmailSent": "OTP sendt", "otpEmailSentDescription": "En OTP er sendt til din e-post", @@ -973,7 +973,7 @@ "logoutError": "Feil ved utlogging", "signingAs": "Logget inn som", "serverAdmin": "Serveradministrator", - "managedSelfhosted": "Managed Self-Hosted", + "managedSelfhosted": "Administrert selv-hostet", "otpEnable": "Aktiver tofaktor", "otpDisable": "Deaktiver tofaktor", "logout": "Logg ut", @@ -989,9 +989,9 @@ "actionDeleteSite": "Slett område", "actionGetSite": "Hent område", "actionListSites": "List opp områder", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", + "setupToken": "Oppsetttoken", + "setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.", + "setupTokenRequired": "Oppsetttoken er nødvendig", "actionUpdateSite": "Oppdater område", "actionListSiteRoles": "List opp tillatte områderoller", "actionCreateResource": "Opprett ressurs", @@ -1349,106 +1349,106 @@ "resourceEnableProxy": "Aktiver offentlig proxy", "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", "externalProxyEnabled": "Ekstern proxy aktivert", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", + "addNewTarget": "Legg til nytt mål", + "targetsList": "Liste over mål", + "targetErrorDuplicateTargetFound": "Duplikat av mål funnet", + "httpMethod": "HTTP-metode", + "selectHttpMethod": "Velg HTTP-metode", + "domainPickerSubdomainLabel": "Underdomene", + "domainPickerBaseDomainLabel": "Grunndomene", + "domainPickerSearchDomains": "Søk i domener...", + "domainPickerNoDomainsFound": "Ingen domener funnet", + "domainPickerLoadingDomains": "Laster inn domener...", + "domainPickerSelectBaseDomain": "Velg grunndomene...", + "domainPickerNotAvailableForCname": "Ikke tilgjengelig for CNAME-domener", + "domainPickerEnterSubdomainOrLeaveBlank": "Skriv inn underdomene eller la feltet stå tomt for å bruke grunndomene.", + "domainPickerEnterSubdomainToSearch": "Skriv inn et underdomene for å søke og velge blant tilgjengelige gratis domener.", + "domainPickerFreeDomains": "Gratis domener", + "domainPickerSearchForAvailableDomains": "Søk etter tilgjengelige domener", + "resourceDomain": "Domene", + "resourceEditDomain": "Rediger domene", + "siteName": "Områdenavn", "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogDestinationIP": "Destination IP", - "editInternalResourceDialogDestinationPort": "Destination Port", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "resourcesTableProxyResources": "Proxy-ressurser", + "resourcesTableClientResources": "Klientressurser", + "resourcesTableNoProxyResourcesFound": "Ingen proxy-ressurser funnet.", + "resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.", + "resourcesTableDestination": "Destinasjon", + "resourcesTableTheseResourcesForUseWith": "Disse ressursene er til bruk med", + "resourcesTableClients": "Klienter", + "resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.", + "editInternalResourceDialogEditClientResource": "Rediger klientressurs", + "editInternalResourceDialogUpdateResourceProperties": "Oppdater ressursens egenskaper og målkonfigurasjon for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Ressursegenskaper", + "editInternalResourceDialogName": "Navn", + "editInternalResourceDialogProtocol": "Protokoll", + "editInternalResourceDialogSitePort": "Områdeport", + "editInternalResourceDialogTargetConfiguration": "Målkonfigurasjon", + "editInternalResourceDialogDestinationIP": "Destinasjons-IP", + "editInternalResourceDialogDestinationPort": "Destinasjonsport", + "editInternalResourceDialogCancel": "Avbryt", + "editInternalResourceDialogSaveResource": "Lagre ressurs", + "editInternalResourceDialogSuccess": "Suksess", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Intern ressurs oppdatert vellykket", + "editInternalResourceDialogError": "Feil", + "editInternalResourceDialogFailedToUpdateInternalResource": "Mislyktes å oppdatere intern ressurs", + "editInternalResourceDialogNameRequired": "Navn er påkrevd", + "editInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn", + "editInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1", + "editInternalResourceDialogProxyPortMax": "Proxy-port må være mindre enn 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat", + "editInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1", + "editInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536", + "createInternalResourceDialogNoSitesAvailable": "Ingen tilgjengelige steder", + "createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et konfigureret delnett for å lage interne ressurser.", + "createInternalResourceDialogClose": "Lukk", + "createInternalResourceDialogCreateClientResource": "Opprett klientressurs", + "createInternalResourceDialogCreateClientResourceDescription": "Lag en ny ressurs som blir tilgjengelig for klienter koblet til det valgte området.", + "createInternalResourceDialogResourceProperties": "Ressursegenskaper", + "createInternalResourceDialogName": "Navn", + "createInternalResourceDialogSite": "Område", + "createInternalResourceDialogSelectSite": "Velg område...", + "createInternalResourceDialogSearchSites": "Søk i områder...", + "createInternalResourceDialogNoSitesFound": "Ingen områder funnet.", + "createInternalResourceDialogProtocol": "Protokoll", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIP": "Destination IP", - "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", - "createInternalResourceDialogDestinationPort": "Destination Port", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." + "createInternalResourceDialogSitePort": "Områdeport", + "createInternalResourceDialogSitePortDescription": "Bruk denne porten for å få tilgang til ressursen på området når du er tilkoblet med en klient.", + "createInternalResourceDialogTargetConfiguration": "Målkonfigurasjon", + "createInternalResourceDialogDestinationIP": "Destinasjons-IP", + "createInternalResourceDialogDestinationIPDescription": "IP-adressen til ressursen på områdets nettverk.", + "createInternalResourceDialogDestinationPort": "Destinasjonsport", + "createInternalResourceDialogDestinationPortDescription": "Porten på destinasjons-IP-en der ressursen kan nås.", + "createInternalResourceDialogCancel": "Avbryt", + "createInternalResourceDialogCreateResource": "Opprett ressurs", + "createInternalResourceDialogSuccess": "Suksess", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Intern ressurs opprettet vellykket", + "createInternalResourceDialogError": "Feil", + "createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke opprette intern ressurs", + "createInternalResourceDialogNameRequired": "Navn er påkrevd", + "createInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn", + "createInternalResourceDialogPleaseSelectSite": "Vennligst velg et område", + "createInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1", + "createInternalResourceDialogProxyPortMax": "Proxy-port må være mindre enn 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat", + "createInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1", + "createInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536", + "siteConfiguration": "Konfigurasjon", + "siteAcceptClientConnections": "Godta klientforbindelser", + "siteAcceptClientConnectionsDescription": "Tillat andre enheter å koble seg til gjennom denne Newt-instansen som en gateway ved hjelp av klienter.", + "siteAddress": "Områdeadresse", + "siteAddressDescription": "Angi IP-adressen til verten for klienter å koble seg til. Dette er den interne adressen til området i Pangolin-nettverket for klienter som adresserer. Må falle innenfor Org-underettet.", + "autoLoginExternalIdp": "Automatisk innlogging med ekstern IDP", + "autoLoginExternalIdpDescription": "Omdiriger brukeren umiddelbart til den eksterne IDP-en for autentisering.", + "selectIdp": "Velg IDP", + "selectIdpPlaceholder": "Velg en IDP...", + "selectIdpRequired": "Vennligst velg en IDP når automatisk innlogging er aktivert.", + "autoLoginTitle": "Omdirigering", + "autoLoginDescription": "Omdirigerer deg til den eksterne identitetsleverandøren for autentisering.", + "autoLoginProcessing": "Forbereder autentisering...", + "autoLoginRedirecting": "Omdirigerer til innlogging...", + "autoLoginError": "Feil ved automatisk innlogging", + "autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.", + "autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL." }