diff --git a/.eslintrc.json b/.eslintrc.json index a9468a44..98ef693e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,3 @@ { - "extends": [ - "next/core-web-vitals", - "next/typescript" - ] + "extends": ["next/core-web-vitals", "next/typescript"] } diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 710c5c86..8af8625d 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 @@ -56,7 +56,7 @@ jobs: shell: bash - name: Install Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: 1.24 @@ -135,6 +135,13 @@ jobs: docker://$DOCKERHUB_IMAGE:$TAG \ docker://$GHCR_IMAGE:$TAG shell: bash + + - name: Login to GitHub Container Registry (for cosign) + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Install cosign # cosign is used to sign and verify container images (key and keyless) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 46c5e8ee..98f9b1c8 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -21,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Node.js - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: '22' diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 5b6889da..4df7e93e 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -14,7 +14,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: days-before-stale: 14 days-before-close: 14 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dda739ce..3b0627cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,9 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: '22' diff --git a/.nvmrc b/.nvmrc index 7273c0fa..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -25 +24 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..0ae563ee --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +.github/ +bruno/ +cli/ +config/ +messages/ +next.config.mjs/ +public/ +tailwind.config.js/ +test/ +**/*.yml +**/*.yaml +**/*.md \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..974188b8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..77440d96 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "editor.codeActionsOnSave": { + "source.addMissingImports.ts": "always" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.formatOnSave": true +} diff --git a/Dockerfile b/Dockerfile index 0644a1c6..fa2d71c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:25-alpine AS builder +FROM node:24-alpine AS builder WORKDIR /app @@ -43,7 +43,7 @@ RUN test -f dist/server.mjs RUN npm run build:cli -FROM node:25-alpine AS runner +FROM node:24-alpine AS runner WORKDIR /app diff --git a/components.json b/components.json index 97d8c8c0..13f7efef 100644 --- a/components.json +++ b/components.json @@ -17,4 +17,4 @@ "lib": "@/lib", "hooks": "@/hooks" } -} \ No newline at end of file +} diff --git a/drizzle.pg.config.ts b/drizzle.pg.config.ts index febd5f45..ba4ca8fe 100644 --- a/drizzle.pg.config.ts +++ b/drizzle.pg.config.ts @@ -1,9 +1,7 @@ import { defineConfig } from "drizzle-kit"; import path from "path"; -const schema = [ - path.join("server", "db", "pg", "schema"), -]; +const schema = [path.join("server", "db", "pg", "schema")]; export default defineConfig({ dialect: "postgresql", diff --git a/drizzle.sqlite.config.ts b/drizzle.sqlite.config.ts index 4912c256..d8344f94 100644 --- a/drizzle.sqlite.config.ts +++ b/drizzle.sqlite.config.ts @@ -2,9 +2,7 @@ import { APP_PATH } from "@server/lib/consts"; import { defineConfig } from "drizzle-kit"; import path from "path"; -const schema = [ - path.join("server", "db", "sqlite", "schema"), -]; +const schema = [path.join("server", "db", "sqlite", "schema")]; export default defineConfig({ dialect: "sqlite", diff --git a/esbuild.mjs b/esbuild.mjs index 7f67fe81..0157c34a 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -24,20 +24,20 @@ const argv = yargs(hideBin(process.argv)) alias: "e", describe: "Entry point file", type: "string", - demandOption: true, + demandOption: true }) .option("out", { alias: "o", describe: "Output file path", type: "string", - demandOption: true, + demandOption: true }) .option("build", { alias: "b", describe: "Build type (oss, saas, enterprise)", type: "string", choices: ["oss", "saas", "enterprise"], - default: "oss", + default: "oss" }) .help() .alias("help", "h").argv; @@ -66,7 +66,9 @@ function privateImportGuardPlugin() { // Check if the importing file is NOT in server/private const normalizedImporter = path.normalize(importingFile); - const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private")); + const isInServerPrivate = normalizedImporter.includes( + path.normalize("server/private") + ); if (!isInServerPrivate) { const violation = { @@ -79,8 +81,8 @@ function privateImportGuardPlugin() { console.log(`PRIVATE IMPORT VIOLATION:`); console.log(` File: ${importingFile}`); console.log(` Import: ${args.path}`); - console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`); - console.log(''); + console.log(` Resolve dir: ${args.resolveDir || "N/A"}`); + console.log(""); } // Return null to let the default resolver handle it @@ -89,16 +91,20 @@ function privateImportGuardPlugin() { build.onEnd((result) => { if (violations.length > 0) { - console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`); + console.log( + `\nSUMMARY: Found ${violations.length} private import violation(s):` + ); violations.forEach((v, i) => { - console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`); + console.log( + ` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}` + ); }); - console.log(''); + console.log(""); result.errors.push({ text: `Private import violations detected: ${violations.length} violation(s) found`, location: null, - notes: violations.map(v => ({ + notes: violations.map((v) => ({ text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`, location: null })) @@ -121,7 +127,9 @@ function dynamicImportGuardPlugin() { // Check if the importing file is NOT in server/private const normalizedImporter = path.normalize(importingFile); - const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private")); + const isInServerPrivate = normalizedImporter.includes( + path.normalize("server/private") + ); if (isInServerPrivate) { const violation = { @@ -134,8 +142,8 @@ function dynamicImportGuardPlugin() { console.log(`DYNAMIC IMPORT VIOLATION:`); console.log(` File: ${importingFile}`); console.log(` Import: ${args.path}`); - console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`); - console.log(''); + console.log(` Resolve dir: ${args.resolveDir || "N/A"}`); + console.log(""); } // Return null to let the default resolver handle it @@ -144,16 +152,20 @@ function dynamicImportGuardPlugin() { build.onEnd((result) => { if (violations.length > 0) { - console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`); + console.log( + `\nSUMMARY: Found ${violations.length} dynamic import violation(s):` + ); violations.forEach((v, i) => { - console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`); + console.log( + ` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}` + ); }); - console.log(''); + console.log(""); result.errors.push({ text: `Dynamic import violations detected: ${violations.length} violation(s) found`, location: null, - notes: violations.map(v => ({ + notes: violations.map((v) => ({ text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`, location: null })) @@ -172,21 +184,28 @@ function dynamicImportSwitcherPlugin(buildValue) { const switches = []; build.onStart(() => { - console.log(`Dynamic import switcher using build type: ${buildValue}`); + console.log( + `Dynamic import switcher using build type: ${buildValue}` + ); }); build.onResolve({ filter: /^#dynamic\// }, (args) => { // Extract the path after #dynamic/ - const dynamicPath = args.path.replace(/^#dynamic\//, ''); + const dynamicPath = args.path.replace(/^#dynamic\//, ""); // Determine the replacement based on build type let replacement; if (buildValue === "oss") { replacement = `#open/${dynamicPath}`; - } else if (buildValue === "saas" || buildValue === "enterprise") { + } else if ( + buildValue === "saas" || + buildValue === "enterprise" + ) { replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private } else { - console.warn(`Unknown build type '${buildValue}', defaulting to #open/`); + console.warn( + `Unknown build type '${buildValue}', defaulting to #open/` + ); replacement = `#open/${dynamicPath}`; } @@ -201,8 +220,10 @@ function dynamicImportSwitcherPlugin(buildValue) { console.log(`DYNAMIC IMPORT SWITCH:`); console.log(` File: ${args.importer}`); console.log(` Original: ${args.path}`); - console.log(` Switched to: ${replacement} (build: ${buildValue})`); - console.log(''); + console.log( + ` Switched to: ${replacement} (build: ${buildValue})` + ); + console.log(""); // Rewrite the import path and let the normal resolution continue return build.resolve(replacement, { @@ -215,12 +236,18 @@ function dynamicImportSwitcherPlugin(buildValue) { build.onEnd((result) => { if (switches.length > 0) { - console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`); + console.log( + `\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':` + ); switches.forEach((s, i) => { - console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`); - console.log(` ${s.originalPath} → ${s.replacementPath}`); + console.log( + ` ${i + 1}. ${path.relative(process.cwd(), s.file)}` + ); + console.log( + ` ${s.originalPath} → ${s.replacementPath}` + ); }); - console.log(''); + console.log(""); } }); } @@ -235,7 +262,7 @@ esbuild format: "esm", minify: false, banner: { - js: banner, + js: banner }, platform: "node", external: ["body-parser"], @@ -244,20 +271,22 @@ esbuild dynamicImportGuardPlugin(), dynamicImportSwitcherPlugin(argv.build), nodeExternalsPlugin({ - packagePath: getPackagePaths(), - }), + packagePath: getPackagePaths() + }) ], sourcemap: "inline", - target: "node22", + target: "node22" }) .then((result) => { // Check if there were any errors in the build result if (result.errors && result.errors.length > 0) { - console.error(`Build failed with ${result.errors.length} error(s):`); + console.error( + `Build failed with ${result.errors.length} error(s):` + ); result.errors.forEach((error, i) => { console.error(`${i + 1}. ${error.text}`); if (error.notes) { - error.notes.forEach(note => { + error.notes.forEach((note) => { console.error(` - ${note.text}`); }); } diff --git a/eslint.config.js b/eslint.config.js index dfc194bc..ae921d45 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,19 +1,19 @@ -import tseslint from 'typescript-eslint'; +import tseslint from "typescript-eslint"; export default tseslint.config({ - files: ["**/*.{ts,tsx,js,jsx}"], - languageOptions: { - parser: tseslint.parser, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true - } + files: ["**/*.{ts,tsx,js,jsx}"], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true + } + } + }, + rules: { + semi: "error", + "prefer-const": "warn" } - }, - rules: { - "semi": "error", - "prefer-const": "warn" - } -}); \ No newline at end of file +}); diff --git a/install/go.mod b/install/go.mod index b639e847..bcd56896 100644 --- a/install/go.mod +++ b/install/go.mod @@ -3,8 +3,8 @@ module installer go 1.24.0 require ( - golang.org/x/term v0.37.0 + golang.org/x/term v0.38.0 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.38.0 // indirect +require golang.org/x/sys v0.39.0 // indirect diff --git a/install/go.sum b/install/go.sum index 24b1a1ef..5655d91a 100644 --- a/install/go.sum +++ b/install/go.sum @@ -1,7 +1,7 @@ -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= diff --git a/install/main.go b/install/main.go index a8cb13e1..e1994fc2 100644 --- a/install/main.go +++ b/install/main.go @@ -54,8 +54,8 @@ type Config struct { type SupportedContainer string const ( - Docker SupportedContainer = "docker" - Podman SupportedContainer = "podman" + Docker SupportedContainer = "docker" + Podman SupportedContainer = "podman" Undefined SupportedContainer = "undefined" ) @@ -160,7 +160,7 @@ func main() { } else { alreadyInstalled = true fmt.Println("Looks like you already installed Pangolin!") - + // Check if MaxMind database exists and offer to update it fmt.Println("\n=== MaxMind Database Update ===") if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil { @@ -209,8 +209,8 @@ func main() { parsedURL, err := url.Parse(appConfig.DashboardURL) if err != nil { - fmt.Printf("Error parsing URL: %v\n", err) - return + fmt.Printf("Error parsing URL: %v\n", err) + return } config.DashboardDomain = parsedURL.Hostname() @@ -242,7 +242,7 @@ func main() { } } - if !alreadyInstalled { + if !alreadyInstalled || config.DoCrowdsecInstall { // Setup Token Section fmt.Println("\n=== Setup Token ===") @@ -359,7 +359,7 @@ func collectUserInput(reader *bufio.Reader) Config { 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", "") + config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "") } // Validate required fields @@ -371,6 +371,10 @@ func collectUserInput(reader *bufio.Reader) Config { fmt.Println("Error: Let's Encrypt email is required") os.Exit(1) } + if config.EnableEmail && config.EmailNoReply == "" { + fmt.Println("Error: No-reply email address is required when email is enabled") + os.Exit(1) + } // Advanced configuration @@ -643,28 +647,28 @@ func checkPortsAvailable(port int) error { func downloadMaxMindDatabase() error { fmt.Println("Downloading MaxMind GeoLite2 Country database...") - + // Download the GeoLite2 Country database - if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz", + if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz", "https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil { return fmt.Errorf("failed to download GeoLite2 database: %v", err) } - + // Extract the database if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil { return fmt.Errorf("failed to extract GeoLite2 database: %v", err) } - + // Find the .mmdb file and move it to the config directory if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil { return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err) } - + // Clean up the downloaded files if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil { fmt.Printf("Warning: failed to clean up temporary files: %v\n", err) } - + fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!") return nil } diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 7c31feb5..5deb59e3 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -1,12 +1,12 @@ { - "setupCreate": "Създайте своя организация, сайт и ресурси", + "setupCreate": "Създайте организацията, сайта и ресурсите", "setupNewOrg": "Нова организация", "setupCreateOrg": "Създаване на организация", "setupCreateResources": "Създаване на ресурси", "setupOrgName": "Име на организацията", - "orgDisplayName": "Това е публичното име на вашата организация.", + "orgDisplayName": "Това е публичното име на организацията.", "orgId": "Идентификатор на организация", - "setupIdentifierMessage": "Това е уникалният идентификатор на вашата организация. Това е различно от публичното ѝ име.", + "setupIdentifierMessage": "Това е уникалният идентификатор за организацията.", "setupErrorIdentifier": "Идентификаторът на организация вече е зает. Моля, изберете друг.", "componentsErrorNoMemberCreate": "В момента не сте част от организация. Създайте организация, за да продължите.", "componentsErrorNoMember": "В момента не сте част от организация.", @@ -50,10 +50,10 @@ "siteMessageRemove": "След премахване, сайтът вече няма да бъде достъпен. Всички цели, свързани със сайта, също ще бъдат премахнати.", "siteQuestionRemove": "Сигурни ли сте, че искате да премахнете сайта от организацията?", "siteManageSites": "Управление на сайтове", - "siteDescription": "Позволете свързване към вашата мрежа чрез сигурни тунели", + "siteDescription": "Създайте и управлявайте сайтове, за да осигурите свързаност със частни мрежи", "siteCreate": "Създайте сайт", "siteCreateDescription2": "Следвайте стъпките по-долу, за да създадете и свържете нов сайт", - "siteCreateDescription": "Създайте нов сайт, за да започнете да свързвате вашите ресурси", + "siteCreateDescription": "Създайте нов сайт, за да започнете да свързвате ресурси", "close": "Затвори", "siteErrorCreate": "Грешка при създаване на сайт", "siteErrorCreateKeyPair": "Ключова двойка или настройки по подразбиране на сайта не са намерени", @@ -74,7 +74,7 @@ "siteInstallNewt": "Инсталирайте Newt", "siteInstallNewtDescription": "Пуснете Newt на вашата система", "WgConfiguration": "WireGuard конфигурация", - "WgConfigurationDescription": "Използвайте следната конфигурация, за да се свържете с вашата мрежа", + "WgConfigurationDescription": "Използвайте следната конфигурация, за да се свържете с мрежата", "operatingSystem": "Операционна система", "commands": "Команди", "recommended": "Препоръчано", @@ -87,32 +87,32 @@ "siteUpdated": "Сайтът е обновен", "siteUpdatedDescription": "Сайтът е актуализиран.", "siteGeneralDescription": "Конфигурирайте общи настройки за този сайт", - "siteSettingDescription": "Настройте настройките на вашия сайт", + "siteSettingDescription": "Конфигурирайте настройките на сайта", "siteSetting": "Настройки на {siteName}", - "siteNewtTunnel": "Newt тунел (Препоръчително)", - "siteNewtTunnelDescription": "Най-лесният начин да създадете входна точка в мрежата си. Без допълнително конфигуриране.", + "siteNewtTunnel": "Нов Сайт (Препоръчително)", + "siteNewtTunnelDescription": "Най-лесният начин да създадете точка за достъп до всяка мрежа. Няма нужда от допълнителни настройки.", "siteWg": "Основен WireGuard", "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteWgDescriptionSaas": "Използвайте всеки WireGuard клиент за установяване на тунел. Ръчно нат задаване е необходимо. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.", "siteLocalDescription": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescriptionSaas": "Само локални ресурси. Без тунелиране. Достъпно само на отдалечени възли.", "siteSeeAll": "Вижте всички сайтове", - "siteTunnelDescription": "Определете как искате да се свържете с вашия сайт", - "siteNewtCredentials": "Newt Удостоверения", - "siteNewtCredentialsDescription": "Това е така, защото Newt ще се удостовери със сървъра", - "siteCredentialsSave": "Запазете вашите удостоверения", + "siteTunnelDescription": "Определете как искате да се свържете със сайта", + "siteNewtCredentials": "Пълномощия", + "siteNewtCredentialsDescription": "Това е как сайтът ще се удостоверява с сървъра", + "siteCredentialsSave": "Запазете Пълномощията", "siteCredentialsSaveDescription": "Ще можете да виждате това само веднъж. Уверете се да го копирате на сигурно място.", "siteInfo": "Информация за сайта", "status": "Статус", "shareTitle": "Управление на връзки за споделяне", - "shareDescription": "Създайте споделяеми връзки, за да разрешите временен или постоянен достъп до вашите ресурси", + "shareDescription": "Създайте споделими връзки, за да предоставите временен или постоянен достъп до прокси ресурсите", "shareSearch": "Търсене на връзки за споделяне...", "shareCreate": "Създайте връзка за споделяне", "shareErrorDelete": "Неуспешно изтриване на връзката", "shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката", "shareDeleted": "Връзката беше изтрита", "shareDeletedDescription": "Връзката беше премахната", - "shareTokenDescription": "Вашият достъп токен може да се предава по два начина: като параметър на URL или в заглавките на заявката. Тези трябва да се предават от клиента при всяка заявка за удостоверен достъп.", + "shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.", "accessToken": "Достъп Токен", "usageExamples": "Примери за използване", "tokenId": "Токен ID", @@ -121,7 +121,7 @@ "importantNote": "Важно бележка", "shareImportantDescription": "По съображения за сигурност, използването на заглавки се препоръчва пред параметри на заявка, когато е възможно, тъй като параметри на заявка могат да бъдат записвани в логове на сървъра или в историята на браузъра.", "token": "Токен", - "shareTokenSecurety": "Пазете вашият достъп токен в безопасност. Не го споделяйте в публичнодостъпни зони или клиентски код.", + "shareTokenSecurety": "Запазете сигурността на токена за достъп. Не го споделяйте в общодостъпни зони или в клиентски код.", "shareErrorFetchResource": "Неуспешно вземане на ресурси", "shareErrorFetchResourceDescription": "Възникна грешка при опит за вземане на ресурсите", "shareErrorCreate": "Неуспешно създаване на връзка за споделяне", @@ -144,8 +144,10 @@ "expires": "Изтича", "never": "Никога", "shareErrorSelectResource": "Моля, изберете ресурс", - "resourceTitle": "Управление на ресурси", - "resourceDescription": "Създайте сигурни проксита към вашите частни приложения", + "proxyResourceTitle": "Управление на обществени ресурси", + "proxyResourceDescription": "Създайте и управлявайте ресурси, които са общодостъпни чрез уеб браузър.", + "clientResourceTitle": "Управление на частни ресурси", + "clientResourceDescription": "Създайте и управлявайте ресурси, които са достъпни само чрез свързан клиент.", "resourcesSearch": "Търсене на ресурси...", "resourceAdd": "Добавете ресурс", "resourceErrorDelte": "Грешка при изтриване на ресурс", @@ -155,9 +157,9 @@ "resourceMessageRemove": "След като се премахне, ресурсът няма повече да бъде достъпен. Всички цели, свързани с ресурса, също ще бъдат премахнати.", "resourceQuestionRemove": "Сигурни ли сте, че искате да премахнете ресурса от организацията?", "resourceHTTP": "HTTPS ресурс", - "resourceHTTPDescription": "Прокси заявки към вашето приложение през HTTPS с помощта на субдомейн или базов домейн.", + "resourceHTTPDescription": "Прокси заяви към приложението по HTTPS използвайки поддомейн или базов домейн.", "resourceRaw": "Суров TCP/UDP ресурс", - "resourceRawDescription": "Прокси заявки към вашето приложение през TCP/UDP с помощта на номер на порт.", + "resourceRawDescription": "Прокси заяви към приложението по TCP/UDP използвайки номер на порт. Това работи само когато сайтовете са свързани към възли.", "resourceCreate": "Създайте ресурс", "resourceCreateDescription": "Следвайте стъпките по-долу, за да създадете нов ресурс", "resourceSeeAll": "Вижте всички ресурси", @@ -171,22 +173,22 @@ "noCountryFound": "Не е намерена държава.", "siteSelectionDescription": "Този сайт ще осигури свързаност до целта.", "resourceType": "Тип ресурс", - "resourceTypeDescription": "Определете как искате да получите достъп до вашия ресурс", + "resourceTypeDescription": "Определете как да се достъпи ресурсът", "resourceHTTPSSettings": "HTTPS настройки", - "resourceHTTPSSettingsDescription": "Конфигурирайте как вашият ресурс ще бъде достъпен през HTTPS", + "resourceHTTPSSettingsDescription": "Конфигурирайте как ресурсът ще бъде достъпен по HTTPS", "domainType": "Тип домейн", "subdomain": "Субдомейн", "baseDomain": "Базов домейн", - "subdomnainDescription": "Субдомейнът, в който ще бъде достъпен вашият ресурс.", + "subdomnainDescription": "Поддомейнът, където ресурсът ще бъде достъпен.", "resourceRawSettings": "TCP/UDP настройки", - "resourceRawSettingsDescription": "Настройте как ресурсът ви ще бъде достъпен през TCP/UDP. Свързвате ресурса към порт на хост сървъра Pangolin, за да го достъпвате от server-public-ip:mapped-port.", + "resourceRawSettingsDescription": "Конфигурирайте как ресурсът ще бъде достъпен чрез TCP/UDP", "protocol": "Протокол", "protocolSelect": "Изберете протокол", "resourcePortNumber": "Номер на порт", "resourcePortNumberDescription": "Външен номер на порт за прокси заявки.", "cancel": "Отмяна", "resourceConfig": "Конфигурационни фрагменти", - "resourceConfigDescription": "Копирайте и поставете тези конфигурационни фрагменти за настройка на вашия TCP/UDP ресурс", + "resourceConfigDescription": "Копирайте и поставете тези конфигурационни отрязъци, за да настроите TCP/UDP ресурса", "resourceAddEntrypoints": "Traefik: Добавете Входни точки", "resourceExposePorts": "Gerbil: Изложете портове в Docker Compose", "resourceLearnRaw": "Научете как да конфигурирате TCP/UDP ресурси", @@ -202,14 +204,14 @@ "proxy": "Прокси", "internal": "Вътрешен", "rules": "Правила", - "resourceSettingDescription": "Конфигурирайте настройките на вашия ресурс", + "resourceSettingDescription": "Конфигурирайте настройките на ресурса", "resourceSetting": "Настройки на {resourceName}", - "alwaysAllow": "Винаги позволявай", - "alwaysDeny": "Винаги отказвай", + "alwaysAllow": "Заобикаляне на Ауторизацията", + "alwaysDeny": "Блокиране на Достъпа", "passToAuth": "Прехвърляне към удостоверяване", - "orgSettingsDescription": "Конфигурирайте общите настройки на вашата организация", + "orgSettingsDescription": "Конфигурирайте настройките на организацията", "orgGeneralSettings": "Настройки на организацията", - "orgGeneralSettingsDescription": "Управлявайте детайлите и конфигурацията на вашата организация", + "orgGeneralSettingsDescription": "Управлявайте детайлите и конфигурацията на организацията", "saveGeneralSettings": "Запазете общите настройки", "saveSettings": "Запазване на настройките", "orgDangerZone": "Опасна зона", @@ -232,7 +234,7 @@ "orgMissing": "Липсва идентификатор на организация", "orgMissingMessage": "Невъзможност за регенериране на покана без идентификатор на организация.", "accessUsersManage": "Управление на потребители", - "accessUsersDescription": "Поканете потребители и ги добавете в роли, за да управлявате достъпа до вашата организация", + "accessUsersDescription": "Канете и управлявайте потребители с достъп до тази организация", "accessUsersSearch": "Търсене на потребители...", "accessUserCreate": "Създайте потребител", "accessUserRemove": "Премахнете потребител", @@ -241,13 +243,13 @@ "role": "Роля", "nameRequired": "Името е задължително", "accessRolesManage": "Управление на роли", - "accessRolesDescription": "Конфигурирайте роли, за да управлявате достъпа до вашата организация", + "accessRolesDescription": "Създайте и управлявайте роли за потребители в организацията", "accessRolesSearch": "Търсене на роли...", "accessRolesAdd": "Добавете роля", "accessRoleDelete": "Изтриване на роля", "description": "Описание", "inviteTitle": "Отворени покани", - "inviteDescription": "Управление на вашите покани към други потребители", + "inviteDescription": "Управлявайте покани за други потребители да се присъединят към организацията", "inviteSearch": "Търсене на покани...", "minutes": "Минути", "hours": "Часове", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Грешка при създаване на API ключ", "apiKeysErrorSetPermission": "Грешка при задаване на разрешения", "apiKeysCreate": "Генерирайте API ключ", - "apiKeysCreateDescription": "Генерирайте нов API ключ за вашата организация", + "apiKeysCreateDescription": "Създайте нов API ключ за организацията", "apiKeysGeneralSettings": "Разрешения", "apiKeysGeneralSettingsDescription": "Определете какво може да прави този API ключ", - "apiKeysList": "Вашият API ключ", - "apiKeysSave": "Запазване на вашия API ключ", + "apiKeysList": "Нов API Ключ", + "apiKeysSave": "Запазете API Ключа", "apiKeysSaveDescription": "Ще можете да виждате това само веднъж. Уверете се да го копирате на сигурно място.", - "apiKeysInfo": "Вашият API ключ е:", + "apiKeysInfo": "API ключът е:", "apiKeysConfirmCopy": "Копирах API ключа", "generate": "Генериране", "done": "Готово", @@ -424,7 +426,7 @@ "userCreated": "Потребителят е създаден", "userCreatedDescription": "Потребителят беше успешно създаден.", "userTypeInternal": "Вътрешен потребител", - "userTypeInternalDescription": "Поканете потребител да се присъедини директно към вашата организация.", + "userTypeInternalDescription": "Поканете потребител да се присъедини директно към организацията.", "userTypeExternal": "Външен потребител", "userTypeExternalDescription": "Създайте потребител с външен доставчик на идентичност.", "accessUserCreateDescription": "Следвайте стъпките по-долу, за да създадете нов потребител", @@ -436,6 +438,16 @@ "inviteEmailSent": "Изпратете покана по имейл до потребителя", "inviteValid": "Валидна за", "selectDuration": "Изберете продължителност", + "selectResource": "Изберете Ресурс", + "filterByResource": "Филтрирай По Ресурс", + "resetFilters": "Нулиране на Филтрите", + "totalBlocked": "Заявки Блокирани От Pangolin", + "totalRequests": "Общо Заявки", + "requestsByCountry": "Заявки По Държава", + "requestsByDay": "Заявки По Ден", + "blocked": "Блокирани", + "allowed": "Позволени", + "topCountries": "Топ Държави", "accessRoleSelect": "Изберете роля", "inviteEmailSentDescription": "Имейлът е изпратен до потребителя с достъпния линк по-долу. Те трябва да достъпят линка, за да приемат поканата.", "inviteSentDescription": "Потребителят е поканен. Те трябва да достъпят линка по-долу, за да приемат поканата.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Запазване на контролите за достъп", "roles": "Роли", "accessUsersRoles": "Управление на потребители и роли", - "accessUsersRolesDescription": "Поканете потребители и ги добавете към роли, за да управлявате достъпа до вашата организация", + "accessUsersRolesDescription": "Поканете потребители и ги добавете към роли, за да управлявате достъпа до организацията", "key": "Ключ", "createdAt": "Създаден на", "proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.", "proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.", "proxyEnableSSL": "Активиране на SSL", - "proxyEnableSSLDescription": "Активиране на SSL/TLS криптиране за сигурни HTTPS връзки към вашите цели.", + "proxyEnableSSLDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целите.", "target": "Цел", "configureTarget": "Конфигуриране на цели", "targetErrorFetch": "Неуспешно извличане на цели", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Неуспешно актуализиране на целите", "targetsErrorUpdateDescription": "Възникна грешка при актуализиране на целите", "targetTlsUpdate": "Настройките на TLS са актуализирани", - "targetTlsUpdateDescription": "Вашите настройки на TLS бяха успешно актуализирани", + "targetTlsUpdateDescription": "Настройките за TLS бяха успешно актуализирани", "targetErrorTlsUpdate": "Неуспешно актуализиране на настройки на TLS", "targetErrorTlsUpdateDescription": "Възникна грешка при актуализиране на настройки на TLS", "proxyUpdated": "Настройките на прокси са актуализирани", - "proxyUpdatedDescription": "Вашите настройки на прокси бяха успешно актуализирани", + "proxyUpdatedDescription": "Настройките за прокси бяха успешно актуализирани", "proxyErrorUpdate": "Неуспешно актуализиране на настройки на прокси", "proxyErrorUpdateDescription": "Възникна грешка при актуализиране на настройки на прокси", - "targetAddr": "IP / Хост име", + "targetAddr": "Хост", "targetPort": "Порт", "targetProtocol": "Протокол", "targetTlsSettings": "Конфигурация на защитена връзка", - "targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс", + "targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройки за ресурса", "targetTlsSettingsAdvanced": "Разширени TLS настройки", "targetTlsSni": "Имя на TLS сървър", "targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.", "targetTlsSubmit": "Запазване на настройките", "targets": "Конфигурация на целите", - "targetsDescription": "Настройте цели за маршрутиране на трафик към вашите бекенд услуги", + "targetsDescription": "Настройте целите да пренасочват трафика към бекенд услугите", "targetStickySessions": "Активиране на постоянни сесии", "targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.", "methodSelect": "Изберете метод", "targetSubmit": "Добавяне на цел", - "targetNoOne": "Този ресурс няма цели. Добавете цел, за да конфигурирате къде да изпращате заявки към вашия бекенд.", + "targetNoOne": "Този ресурс няма цели. Добавете цел, за да конфигурирате къде да се изпращат заявките към бекенда.", "targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.", "targetsSubmit": "Запазване на целите", "addTarget": "Добавете цел", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Целта беше успешно създадена", "targetErrorCreate": "Неуспешно създаване на целта", "targetErrorCreateDescription": "Възникна грешка при създаването на целта", + "tlsServerName": "TLS Име на Сървъра", + "tlsServerNameDescription": "TLS името на сървъра, което ще се използва за SNI", "save": "Запази", "proxyAdditional": "Допълнителни настройки на прокси", - "proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки", + "proxyAdditionalDescription": "Конфигурирайте как ресурсът обработва настройките на прокси", "proxyCustomHeader": "Персонализиран хост заглавие", "proxyCustomHeaderDescription": "Хост заглавието, което да зададете при прокси заявките. Оставете празно, за да използвате подразбиране.", "proxyAdditionalSubmit": "Запазване на прокси настройките", @@ -558,7 +572,7 @@ "rulesMatchType": "Тип на съвпадение", "value": "Стойност", "rulesAbout": "Относно правилата", - "rulesAboutDescription": "Правилата ви позволяват да контролирате достъпа до вашия ресурс въз основа на набор от критерии. Можете да създавате правила за разрешаване или отказ на достъп въз основа на IP адрес или URL път.", + "rulesAboutDescription": "Правилата ви позволяват да контролирате достъпа до ресурса въз основа на набор от критерии. Можете да създадете правила за позволяване или отказ на достъп въз основа на IP адрес или URL път.", "rulesActions": "Действия", "rulesActionAlwaysAllow": "Винаги позволи: заобикаля всички методи за автентикация", "rulesActionAlwaysDeny": "Винаги отказвай: блокиране на всички заявки; не може да се направи опит за автентикация", @@ -570,7 +584,7 @@ "rulesEnable": "Активирай правилата", "rulesEnableDescription": "Активиране или деактивиране на оценката на правилата за този ресурс", "rulesResource": "Конфигурация на правилата за ресурси", - "rulesResourceDescription": "Конфигурирайте правила, за да контролирате достъпа до вашия ресурс", + "rulesResourceDescription": "Конфигурирайте правила за контролиране на достъпа до ресурса", "ruleSubmit": "Добави правило", "rulesNoOne": "Няма правила. Добавете правило чрез формуляра.", "rulesOrder": "Правилата се оценяват по приоритет в нарастващ ред.", @@ -586,7 +600,7 @@ "none": "Няма", "unknown": "Неизвестно", "resources": "Ресурси", - "resourcesDescription": "Ресурсите са проксита за приложения, работещи във вашата частна мрежа. Създайте ресурс за всеки HTTP/HTTPS или суров TCP/UDP услуга във вашата частна мрежа. Всеки ресурс трябва да бъде свързан със сайт, за да се осигури частна, сигурна свързаност чрез криптиран WireGuard тунел.", + "resourcesDescription": "Ресурсите са прокси към приложения, работещи в частната мрежа. Създайте ресурс за всяка HTTP/HTTPS или сурова TCP/UDP услуга във вашата частна мрежа. Всеки ресурс трябва да бъде свързан със сайт, за да се позволи частна, сигурна свързаност през криптиран WireGuard тунел.", "resourcesWireGuardConnect": "Сигурно свързване с криптиране на WireGuard", "resourcesMultipleAuthenticationMethods": "Конфигуриране на множество методи за автентикация", "resourcesUsersRolesAccess": "Контрол на достъпа, базиран на потребители и роли", @@ -597,7 +611,7 @@ "resourceSelect": "Изберете ресурс", "shareLinks": "Споделени връзки", "share": "Споделени връзки", - "shareDescription2": "Създайте споделени връзки към вашите ресурси. Връзките осигуряват временно или неограничено достъп до вашия ресурс. Можете да конфигурирате продължителността на изтичане на връзката при създаването й.", + "shareDescription2": "Създайте връзки за достъп до ресурси. Връзките предоставят временен или неограничен достъп до вашия ресурс. Можете да конфигурирате продължителността на изтичане на връзката, когато я създавате.", "shareEasyCreate": "Лесно за създаване и споделяне", "shareConfigurableExpirationDuration": "Конфигурируемо време на изтичане", "shareSecureAndRevocable": "Сигурни и отменяеми", @@ -607,19 +621,19 @@ "unknownCommand": "Неизвестна команда", "newtErrorFetchReleases": "Неуспешно получаване на информация за изданието: {err}", "newtErrorFetchLatest": "Грешка при получаването на последното издание: {err}", - "newtEndpoint": "Newt Изходен пункт", - "newtId": "Newt ID", - "newtSecretKey": "Newt Secret Key", + "newtEndpoint": "Крайна точка", + "newtId": "Идентификационен номер", + "newtSecretKey": "Секретен ключ", "architecture": "Архитектура", "sites": "Сайтове", - "siteWgAnyClients": "Използвайте всеки WireGuard клиент за свързване. Ще трябва да адресирате вашите вътрешни ресурси, използвайки IP на равностойния.", + "siteWgAnyClients": "Използвайте клиент на WireGuard, за да се свържете. Ще трябва да използвате вътрешните ресурси чрез IP адреса на връстника.", "siteWgCompatibleAllClients": "Съвместим с всички WireGuard клиенти", "siteWgManualConfigurationRequired": "Необходима е ръчна конфигурация", "userErrorNotAdminOrOwner": "Потребителят не е администратор или собственик", "pangolinSettings": "Настройки - Панголин", "accessRoleYour": "Вашата роля:", - "accessRoleSelect2": "Изберете роля", - "accessUserSelect": "Изберете потребител", + "accessRoleSelect2": "Изберете роли", + "accessUserSelect": "Изберете потребители", "otpEmailEnter": "Въведете имейл", "otpEmailEnterDescription": "Натиснете Enter, за да добавите имейл след като сте го въведели в полето за въвеждане.", "otpEmailErrorInvalid": "Невалиден имейл адрес. Wilcard (*) трябва да е цялата част от локалния адрес.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Задай ПИН код", "resourcePincodeSetupTitleDescription": "Задайте ПИН код, за да защитите този ресурс", "resourceRoleDescription": "Администраторите винаги могат да имат достъп до този ресурс.", - "resourceUsersRoles": "Потребители и роли", + "resourceUsersRoles": "Контроли за достъп", "resourceUsersRolesDescription": "Конфигурирайте кои потребители и роли могат да посещават този ресурс", "resourceUsersRolesSubmit": "Запазете потребители и роли", "resourceWhitelistSave": "Успешно запазено", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Прехвърлете ресурс", "siteDestination": "Дестинационен сайт", "searchSites": "Търси сайтове", + "countries": "Държави", "accessRoleCreate": "Създайте роля", "accessRoleCreateDescription": "Създайте нова роля за групиране на потребители и управление на техните разрешения.", "accessRoleCreateSubmit": "Създайте роля", @@ -766,15 +781,15 @@ "idpOidcConfigure": "Конфигурация на OAuth2/OIDC", "idpOidcConfigureDescription": "Конфигурирайте OAuth2/OIDC доставчика на крайни точки и кредити", "idpClientId": "ID на клиента", - "idpClientIdDescription": "OAuth2 идентификационен номер на клиента от вашия доставчик на идентичност", + "idpClientIdDescription": "OAuth2 идентификационен клиент от доставчика на идентичност", "idpClientSecret": "Секретен код на клиента", - "idpClientSecretDescription": "OAuth2 секретен код на клиента от вашия доставчик на идентичност", + "idpClientSecretDescription": "OAuth2 секретен клиент от доставчика на идентичност", "idpAuthUrl": "URL за удостоверение", "idpAuthUrlDescription": "OAuth2 крайна точка за удостоверяване URL", "idpTokenUrl": "URL на токена", "idpTokenUrlDescription": "OAuth2 крайна точка на токена URL", "idpOidcConfigureAlert": "Важно информация", - "idpOidcConfigureAlertDescription": "След създаването на доставчика на идентичност ще е необходимо да конфигурирате URL за обратна връзка в настройките на вашия доставчик на идентичност. URL за обратна връзка ще бъде предоставен след успешно създаване.", + "idpOidcConfigureAlertDescription": "След създаването на доставчика на идентичност, ще трябва да конфигурирате адреса за callback в настройките на доставчика на идентичност. Адресът за callback ще бъде предоставен след успешно създаване.", "idpToken": "Конфигуриране на токените", "idpTokenDescription": "Конфигурирайте как да извлечете информация за потребителя от ID токена", "idpJmespathAbout": "Относно JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Създайте доставчик на идентичност", "orgPolicies": "Организационни политики", "idpSettings": "{idpName} Настройки", - "idpCreateSettingsDescription": "Конфигурирайте настройките за вашия доставчик на идентичност", + "idpCreateSettingsDescription": "Конфигурирайте настройките за доставчика на идентичност", "roleMapping": "Ролева карта", "orgMapping": "Организационна карта", "orgPoliciesSearch": "Търсене на организационни политики...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Идентификационният доставчик беше актуализиран успешно", "redirectUrl": "URL за пренасочване", "redirectUrlAbout": "За URL за пренасочване", - "redirectUrlAboutDescription": "Това е URL, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL в настройките на вашия доставчик на идентификация.", + "redirectUrlAboutDescription": "Това е URL адресът, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL адрес в настройките на доставчика на идентичност.", "pangolinAuth": "Authent - Pangolin", "verificationCodeLengthRequirements": "Вашият код за удостоверяване трябва да бъде 8 символа.", "errorOccurred": "Възникна грешка", @@ -909,6 +924,10 @@ "passwordResetSent": "Ще изпратим код за нулиране на паролата на този имейл адрес.", "passwordResetCode": "Код за нулиране", "passwordResetCodeDescription": "Проверете имейла си за код за нулиране.", + "generatePasswordResetCode": "Генериране на код за нулиране на парола", + "passwordResetCodeGenerated": "Кодът за нулиране на парола е генериран", + "passwordResetCodeGeneratedDescription": "Споделете този код с потребителя. Той може да го използва, за да нулира паролата си.", + "passwordResetUrl": "URL за нулиране", "passwordNew": "Нова парола", "passwordNewConfirm": "Потвърдете новата парола", "changePassword": "Промяна на парола", @@ -926,6 +945,9 @@ "pincodeAuth": "Код на удостоверителя", "pincodeSubmit2": "Изпрати код", "passwordResetSubmit": "Заявка за нулиране", + "passwordResetAlreadyHaveCode": "Въведете код за нулиране на парола", + "passwordResetSmtpRequired": "Моля, свържете се с вашия администратор", + "passwordResetSmtpRequiredDescription": "Кодът за нулиране на парола е задължителен за нулиране на паролата ви. Моля, свържете се с вашия администратор за помощ.", "passwordBack": "Назад към Парола", "loginBack": "Връщане към вход", "signup": "Регистрация", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Списък на ресурсите на сайта", "actionUpdateSiteResource": "Актуализиране на сайт ресурс", "actionListInvitations": "Списък с покани", + "actionExportLogs": "Експортиране на дневници", + "actionViewLogs": "Преглед на дневници", "noneSelected": "Нищо не е избрано", "orgNotFound2": "Няма намерени организации.", "searchProgress": "Търсене...", "create": "Създаване", "orgs": "Организации", "loginError": "Възникна грешка при влизане", + "loginRequiredForDevice": "Необходим е вход за удостоверяване на вашето устройство.", "passwordForgot": "Забравена парола?", "otpAuth": "Двуфакторно удостоверяване", "otpAuthDescription": "Въведете кода от приложението за удостоверяване или един от вашите резервни кодове за еднократна употреба.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Начало", "sidebarSites": "Сайтове", "sidebarResources": "Ресурси", + "sidebarProxyResources": "Публично", + "sidebarClientResources": "Частно", "sidebarAccessControl": "Контрол на достъпа", + "sidebarLogsAndAnalytics": "Дневници и анализи", "sidebarUsers": "Потребители", + "sidebarAdmin": "Администратор", "sidebarInvitations": "Покани", "sidebarRoles": "Роли", - "sidebarShareableLinks": "Споделени връзки", + "sidebarShareableLinks": "Връзки", "sidebarApiKeys": "API ключове", "sidebarSettings": "Настройки", "sidebarAllUsers": "Всички потребители", "sidebarIdentityProviders": "Идентификационни доставчици", "sidebarLicense": "Лиценз", "sidebarClients": "Клиенти", + "sidebarUserDevices": "Потребители", + "sidebarMachineClients": "Машини", "sidebarDomains": "Домейни", + "sidebarGeneral": "Общи", + "sidebarLogAndAnalytics": "Лог & Анализи", "sidebarBluePrints": "Чертежи", + "sidebarOrganization": "Организация", + "sidebarLogsAnalytics": "Анализи", "blueprints": "Чертежи", "blueprintsDescription": "Прилагайте декларативни конфигурации и преглеждайте предишни изпълнения", "blueprintAdd": "Добави Чертеж", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Вижте резултата от приложените чертежи и всички възникнали грешки", "blueprintInfo": "Информация за Чертежа", "message": "Съобщение", - "blueprintContentsDescription": "Дефинирайте YAML съдържанието, описващо вашата инфраструктура", + "blueprintContentsDescription": "Определете съдържанието на YAML, описващо инфраструктурата", "blueprintErrorCreateDescription": "Възникна грешка при прилагането на чертежа", "blueprintErrorCreate": "Грешка при създаването на чертеж", "searchBlueprintProgress": "Търси чертежи...", @@ -1230,15 +1265,15 @@ "loading": "Зареждане", "restart": "Рестарт", "domains": "Домейни", - "domainsDescription": "Управление на домейни за вашата организация", + "domainsDescription": "Създайте и управлявайте наличните домейни в организацията", "domainsSearch": "Търсене на домейни...", "domainAdd": "Добавяне на домейн", - "domainAddDescription": "Регистриране на нов домейн с вашата организация", + "domainAddDescription": "Регистрирайте нов домейн в организацията", "domainCreate": "Създаване на домейн", "domainCreatedDescription": "Домейнът е създаден успешно", "domainDeletedDescription": "Домейнът е изтрит успешно", - "domainQuestionRemove": "Сигурни ли сте, че искате да премахнете домейна от вашия профил?", - "domainMessageRemove": "След премахването, домейнът вече няма да бъде свързан с вашия профил.", + "domainQuestionRemove": "Сигурни ли сте, че искате да премахнете домейна?", + "domainMessageRemove": "След като бъде премахнат, домейнът вече няма да бъде свързан с организацията.", "domainConfirmDelete": "Потвърдете изтриването на домейн", "domainDelete": "Изтриване на домейн", "domain": "Домейн", @@ -1257,7 +1292,7 @@ "pending": "Чакащо", "sidebarBilling": "Фактуриране", "billing": "Фактуриране", - "orgBillingDescription": "Управление на информацията за фактуриране и абонаментите", + "orgBillingDescription": "Управлявайте информацията за плащане и абонаментите", "github": "GitHub", "pangolinHosted": "Hosted Pangolin", "fossorial": "Fossorial", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Актуализации на продукта", "productUpdateEmpty": "Няма актуализации", "dismissAll": "Отхвърляне на всички", - "pangolinUpdateAvailable": "Налична е нова версия", + "pangolinUpdateAvailable": "Актуализация е налична", "pangolinUpdateAvailableInfo": "Версия {version} е готова за инсталиране", - "pangolinUpdateAvailableReleaseNotes": "Преглед на бележките за издание", + "pangolinUpdateAvailableReleaseNotes": "Преглед на бележките за изданието", "newtUpdateAvailable": "Ново обновление", "newtUpdateAvailableInfo": "Нова версия на Newt е налична. Моля, обновете до последната версия за най-добро изживяване.", "domainPickerEnterDomain": "Домейн", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Проверка на наличността...", - "domainPickerNoMatchingDomains": "Не са намерени съвпадащи домейни. Опитайте се с друг домейн или проверете настройките на домейна на вашата организация.", + "domainPickerNoMatchingDomains": "Не са намерени съвпадащи домейни. Опитайте различен домейн или проверете настройките на домейна на организацията.", "domainPickerOrganizationDomains": "Домейни на организацията", "domainPickerProvidedDomains": "Предоставени домейни", "domainPickerSubdomain": "Поддомейн: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Промяна на абонамента", "billingStartSubscription": "Започване на абонамент", "billingRecurringCharge": "Повтаряща се такса", - "billingManageSubscriptionSettings": "Управление на настройките и предпочитанията на абонамента ви", + "billingManageSubscriptionSettings": "Управление на настройките и предпочитанията за абонамент", "billingNoActiveSubscription": "Нямате активен абонамент. Започнете абонамента си, за да увеличите лимитите за използване.", "billingFailedToLoadSubscription": "Грешка при зареждане на абонамент", "billingFailedToLoadUsage": "Грешка при зареждане на използването", @@ -1345,9 +1380,9 @@ "billingPortalError": "Грешка в портала", "billingDataUsageInfo": "Таксува се за всички данни, прехвърляни през вашите защитени тунели, когато сте свързани към облака. Това включва както входящия, така и изходящия трафик за всички ваши сайтове. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Данните не се таксуват при използване на възли.", "billingOnlineTimeInfo": "Таксува се на база колко време вашите сайтове остават свързани с облака. Пример: 44,640 минути се равняват на един сайт работещ 24/7 за цял месец. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Времето не се таксува при използване на възли.", - "billingUsersInfo": "Таксува се за всеки потребител във вашата организация. Фактурирането се извършва ежедневно на базата на броя активни потребителски акаунти във вашата организация.", - "billingDomainInfo": "Таксува се за всеки домейн във вашата организация. Фактурирането се извършва ежедневно на базата на броя активни домейн акаунти във вашата организация.", - "billingRemoteExitNodesInfo": "Таксува се за всеки управляван възел във вашата организация. Фактурирането се извършва ежедневно на базата на броя активни управлявани възли във вашата организация.", + "billingUsersInfo": "Таксува се всеки потребител в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните потребителски акаунти във вашата организация.", + "billingDomainInfo": "Таксува се всеки домейн в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните домейн акаунти във вашата организация.", + "billingRemoteExitNodesInfo": "Таксува се всеки управляван възел в организацията. Таксуването се изчислява ежедневно въз основа на броя на активните управлявани възли във вашата организация.", "domainNotFound": "Домейнът не е намерен", "domainNotFoundDescription": "Този ресурс е деактивиран, защото домейнът вече не съществува в нашата система. Моля, задайте нов домейн за този ресурс.", "failed": "Неуспешно", @@ -1430,29 +1465,32 @@ "and": "и", "privacyPolicy": "политиката за поверителност" }, + "signUpMarketing": { + "keepMeInTheLoop": "Дръж ме в течение с новини, актуализации и нови функции чрез имейл." + }, "siteRequired": "Изисква се сайт.", "olmTunnel": "Olm тунел", "olmTunnelDescription": "Използвайте Olm за клиентска свързаност", "errorCreatingClient": "Възникна грешка при създаване на клиент", "clientDefaultsNotFound": "Не са намерени настройки по подразбиране за клиента", "createClient": "Създаване на клиент", - "createClientDescription": "Създайте нов клиент за свързване към вашите сайтове", + "createClientDescription": "Създайте нов клиент за достъп до частни ресурси", "seeAllClients": "Виж всички клиенти", "clientInformation": "Информация за клиента", "clientNamePlaceholder": "Име на клиента", "address": "Адрес", "subnetPlaceholder": "Мрежа", - "addressDescription": "Адресът, който клиентът ще използва за свързване", + "addressDescription": "Вътрешният адрес на клиента. Трябва да пада в подмрежата на организацията.", "selectSites": "Избор на сайтове", "sitesDescription": "Клиентът ще има връзка с избраните сайтове", "clientInstallOlm": "Инсталиране на Olm", "clientInstallOlmDescription": "Конфигурирайте Olm да работи на вашата система", - "clientOlmCredentials": "Olm Удостоверения", - "clientOlmCredentialsDescription": "Това е как Olm ще се удостоверява със сървъра", - "olmEndpoint": "Olm Ендпойнт", - "olmId": "Olm ID", - "olmSecretKey": "Olm Тайна парола", - "clientCredentialsSave": "Запазете вашите удостоверения", + "clientOlmCredentials": "Удостоверителни данни", + "clientOlmCredentialsDescription": "Това е начинът, по който клиентът ще се удостоверява със сървъра", + "olmEndpoint": "Крайна точка", + "olmId": "Идентификационен номер", + "olmSecretKey": "Секретен ключ", + "clientCredentialsSave": "Запазете удостоверителните данни", "clientCredentialsSaveDescription": "Ще можете да го видите само веднъж. Уверете се, че ще го копирате на сигурно място.", "generalSettingsDescription": "Конфигурирайте общите настройки за този клиент", "clientUpdated": "Клиентът актуализиран", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Възникна грешка при получаването на сайтовете.", "olmErrorFetchReleases": "Възникна грешка при получаването на Olm версиите.", "olmErrorFetchLatest": "Възникна грешка при получаването на последната версия на Olm.", - "remoteSubnets": "Отдалечени подмрежи", "enterCidrRange": "Въведете CIDR обхват", - "remoteSubnetsDescription": "Добавете CIDR диапазони, които могат да бъдат достъпни от този сайт отдалечено с клиенти. Използвайте формат като 10.0.0.0/24. Това се прилага САМО за VPN клиентска свързаност.", "resourceEnableProxy": "Разрешаване на публичен прокси", "resourceEnableProxyDescription": "Разрешете публично проксиране на този ресурс. Това позволява достъп до ресурса извън мрежата чрез облак на отворен порт. Изисква конфигурация на Traefik.", "externalProxyEnabled": "Външен прокси разрешен", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Мониторинг на здравето на тази цел. Можете да наблюдавате различен краен пункт от целта, ако е необходимо.", "healthScheme": "Метод", "healthSelectScheme": "Избор на метод", + "healthCheckPortInvalid": "Портът за проверка на състоянието трябва да е между 1 и 65535", "healthCheckPath": "Път", "healthHostname": "IP / Хост", "healthPort": "Порт", "healthCheckPathDescription": "Пътят за проверка на здравното състояние.", - "healthyIntervalSeconds": "Интервал за здраве", - "unhealthyIntervalSeconds": "Интервал за нездраве", + "healthyIntervalSeconds": "Интервал на здраве (сек)", + "unhealthyIntervalSeconds": "Интервал на нездраве (сек)", "IntervalSeconds": "Интервал за здраве", - "timeoutSeconds": "Време за изчакване", + "timeoutSeconds": "Време за изчакване (сек)", "timeIsInSeconds": "Времето е в секунди", "retryAttempts": "Опити за повторно", "expectedResponseCodes": "Очаквани кодове за отговор", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Редактиране на домейн", "siteName": "Име на сайта", "proxyPort": "Порт", - "resourcesTableProxyResources": "Прокси Ресурси", - "resourcesTableClientResources": "Клиентски ресурси", + "resourcesTableProxyResources": "Публичен", + "resourcesTableClientResources": "Частен", "resourcesTableNoProxyResourcesFound": "Не са намерени ресурсни проксита.", "resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.", "resourcesTableDestination": "Дестинация", - "resourcesTableTheseResourcesForUseWith": "Тези ресурси са за използване с", + "resourcesTableAlias": "Псевдоним", "resourcesTableClients": "Клиенти", "resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.", "resourcesTableNoTargets": "Без цели", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Извън линия", "resourcesTableUnknown": "Неизвестно", "resourcesTableNotMonitored": "Не е наблюдавано", - "editInternalResourceDialogEditClientResource": "Редактиране на клиентски ресурс", - "editInternalResourceDialogUpdateResourceProperties": "Актуализирайте свойствата на ресурса и конфигурацията на целите за {resourceName}.", + "editInternalResourceDialogEditClientResource": "Редактиране на частен ресурс", + "editInternalResourceDialogUpdateResourceProperties": "Актуализирайте конфигурацията на ресурса и контрола на достъпа за {resourceName}", "editInternalResourceDialogResourceProperties": "Свойствата на ресурса", "editInternalResourceDialogName": "Име", "editInternalResourceDialogProtocol": "Протокол", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Невалиден формат на IP адрес", "editInternalResourceDialogDestinationPortMin": "Дестинационният порт трябва да бъде поне 1", "editInternalResourceDialogDestinationPortMax": "Дестинационният порт трябва да е по-малък от 65536", + "editInternalResourceDialogPortModeRequired": "За порт режим се изискват протокол, прокси порт и порт на дестинация", + "editInternalResourceDialogMode": "Режим", + "editInternalResourceDialogModePort": "Порт", + "editInternalResourceDialogModeHost": "Хост", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Дестинация", + "editInternalResourceDialogDestinationHostDescription": "IP адресът или името на хоста на ресурса в мрежата на сайта.", + "editInternalResourceDialogDestinationIPDescription": "IP адресът или името на хоста на ресурса в мрежата на сайта.", + "editInternalResourceDialogDestinationCidrDescription": "CIDR диапазонът на ресурса в мрежата на сайта.", + "editInternalResourceDialogAlias": "Псевдоним", + "editInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.", "createInternalResourceDialogNoSitesAvailable": "Няма достъпни сайтове", "createInternalResourceDialogNoSitesAvailableDescription": "Трябва да имате поне един сайт на Newt с конфигурирана мрежа, за да създадете вътрешни ресурси.", "createInternalResourceDialogClose": "Затвори", - "createInternalResourceDialogCreateClientResource": "Създаване на клиентски ресурс", - "createInternalResourceDialogCreateClientResourceDescription": "Създайте нов ресурс, който ще бъде достъпен за клиентите свързани със избрания сайт.", + "createInternalResourceDialogCreateClientResource": "Създаване на частен ресурс", + "createInternalResourceDialogCreateClientResourceDescription": "Създайте нов ресурс, който ще бъде достъпен само за клиенти, свързани към организацията", "createInternalResourceDialogResourceProperties": "Свойства на ресурса", "createInternalResourceDialogName": "Име", "createInternalResourceDialogSite": "Сайт", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Невалиден формат на IP адрес", "createInternalResourceDialogDestinationPortMin": "Дестинационният порт трябва да бъде поне 1", "createInternalResourceDialogDestinationPortMax": "Дестинационният порт трябва да е по-малък от 65536", + "createInternalResourceDialogPortModeRequired": "За порт режим се изискват протокол, прокси порт и порт на дестинация", + "createInternalResourceDialogMode": "Режим", + "createInternalResourceDialogModePort": "Порт", + "createInternalResourceDialogModeHost": "Хост", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Дестинация", + "createInternalResourceDialogDestinationHostDescription": "IP адресът или името на хоста на ресурса в мрежата на сайта.", + "createInternalResourceDialogDestinationCidrDescription": "CIDR диапазонът на ресурса в мрежата на сайта.", + "createInternalResourceDialogAlias": "Псевдоним", + "createInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.", "siteConfiguration": "Конфигурация", "siteAcceptClientConnections": "Приемане на клиентски връзки", - "siteAcceptClientConnectionsDescription": "Позволете на други устройства да се свързват чрез този Newt инстанция като възел чрез клиенти.", - "siteAddress": "Адрес на сайта", - "siteAddressDescription": "Посочете IP адреса на хоста, към който клиентите ще се свързват. Това е вътрешният адрес на сайта в мрежата на Панголиин за адресиране от клиенти. Трябва да е в рамките на подмрежата на Организацията.", + "siteAcceptClientConnectionsDescription": "Позволете на потребителските устройства и клиенти да получават достъп до ресурси на този сайт. Това може да бъде променено по-късно.", + "siteAddress": "Адрес на сайта (Разширено)", + "siteAddressDescription": "Вътрешният адрес на сайта. Трябва да пада в подмрежата на организацията.", + "siteNameDescription": "Показваното име на сайта, което може да се промени по-късно.", "autoLoginExternalIdp": "Автоматично влизане с Външен IDP", "autoLoginExternalIdpDescription": "Незабавно пренасочете потребителя към външния IDP за удостоверяване.", "selectIdp": "Изберете IDP", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.", "autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.", "remoteExitNodeManageRemoteExitNodes": "Отдалечени възли", - "remoteExitNodeDescription": "Самостоятелно хоствайте един или повече отдалечени възли, за да разширите своята мрежова връзка и намалите зависимостта от облака.", + "remoteExitNodeDescription": "Самостоятелно хоствайте един или повече отдалечени възли, за да разширите мрежовата свързаност и да намалите зависимостта от облака", "remoteExitNodes": "Възли", "searchRemoteExitNodes": "Търсене на възли...", "remoteExitNodeAdd": "Добавяне на възел", @@ -1623,11 +1682,11 @@ "sidebarRemoteExitNodes": "Отдалечени възли", "remoteExitNodeCreate": { "title": "Създаване на възел", - "description": "Създайте нов възел, за да разширите мрежовата си свързаност", + "description": "Създайте нов възел, за да разширите мрежовата свързаност", "viewAllButton": "Вижте всички възли", "strategy": { "title": "Стратегия на създаване", - "description": "Изберете това, за да конфигурирате ръчно възела си или да създадете нови кредити.", + "description": "Изберете това, за да конфигурирате ръчно възела или да генерирате нови идентификационни данни.", "adopt": { "title": "Осиновете възел", "description": "Изберете това, ако вече имате кредити за възела." @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Генерирани кредити", - "description": "Използвайте тези генерирани кредити, за да конфигурирате възела си", + "description": "Използвайте тези генерирани идентификационни данни за конфигуриране на възела", "nodeIdTitle": "ID на възела", "secretTitle": "Секретен", "saveCredentialsTitle": "Добавете кредити към конфигурацията", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Тип на доставчика на идентичност", "roleMappingExpressionPlaceholder": "напр.: contains(groups, 'admin') && 'Admin' || 'Member'", "idpGoogleConfiguration": "Конфигурация на Google", - "idpGoogleConfigurationDescription": "Конфигурирайте своите Google OAuth2 кредити", - "idpGoogleClientIdDescription": "Вашият Google OAuth2 клиентски ID", - "idpGoogleClientSecretDescription": "Вашият Google OAuth2 клиентски секрет", + "idpGoogleConfigurationDescription": "Конфигурирайте Google OAuth2 идентификационни данни", + "idpGoogleClientIdDescription": "Google OAuth2 идентификационен клиент", + "idpGoogleClientSecretDescription": "Google OAuth2 секретен клиент", "idpAzureConfiguration": "Конфигурация на Azure Entra ID", - "idpAzureConfigurationDescription": "Конфигурирайте своите Azure Entra ID OAuth2 кредити", + "idpAzureConfigurationDescription": "Конфигурирайте OAuth2 идентификационни данни на Azure Entra ID", "idpTenantId": "Идентификационен номер на наемателя", - "idpTenantIdPlaceholder": "вашият идентификационен номер на наемателя", - "idpAzureTenantIdDescription": "Вашият Azure идентификационен номер на наемателя (намира се в преглед на Azure Active Directory)", - "idpAzureClientIdDescription": "Вашият Azure клиентски идентификационен номер за приложението", - "idpAzureClientSecretDescription": "Вашият Azure клиентски секрет за приложението", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "Идентификационен номер на наемателя на Azure (намира се в прегледа на Azure Active Directory)", + "idpAzureClientIdDescription": "Идентификационен код на клиента за регистриране на приложение в Azure", + "idpAzureClientSecretDescription": "Секретен код на клиента за регистриране на приложение в Azure", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Конфигурация на Google", "idpAzureConfigurationTitle": "Конфигурация на Azure Entra ID", "idpTenantIdLabel": "Идентификационен номер на наемателя", - "idpAzureClientIdDescription2": "Вашият Azure клиентски идентификационен номер за приложението", - "idpAzureClientSecretDescription2": "Вашият Azure клиентски секрет за приложението", + "idpAzureClientIdDescription2": "Идентификационен код на клиента за регистриране на приложение в Azure", + "idpAzureClientSecretDescription2": "Секретен код на клиента за регистриране на приложение в Azure", "idpGoogleDescription": "Google OAuth2/OIDC доставчик", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC доставчик", "subnet": "Подмрежа", "subnetDescription": "Подмрежата за конфигурацията на мрежата на тази организация.", "authPage": "Страница за удостоверяване", - "authPageDescription": "Конфигурирайте страницата за удостоверяване на вашата организация", + "authPageDescription": "Конфигурирайте страницата за автентикация за организацията", "authPageDomain": "Домен на страницата за удостоверяване", "noDomainSet": "Няма зададен домейн", "changeDomain": "Смяна на домейн", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Задаване на домейн на страницата за удостоверяване", "failedToFetchCertificate": "Неуспех при извличане на сертификат", "failedToRestartCertificate": "Неуспех при рестартиране на сертификат", - "addDomainToEnableCustomAuthPages": "Добавете домейн, за да активирате персонализирани страници за удостоверяване за вашата организация", + "addDomainToEnableCustomAuthPages": "Добавете домейн за да активирате персонализирани страници за автентикация за организацията", "selectDomainForOrgAuthPage": "Изберете домейн за страницата за удостоверяване на организацията", "domainPickerProvidedDomain": "Предоставен домейн", "domainPickerFreeProvidedDomain": "Безплатен предоставен домейн", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" не може да се направи валиден за {domain}.", "domainPickerSubdomainSanitized": "Поддомен пречистен", "domainPickerSubdomainCorrected": "\"{sub}\" беше коригиран на \"{sanitized}\"", - "orgAuthSignInTitle": "Впишете се във вашата организация", + "orgAuthSignInTitle": "Влезте в организацията", "orgAuthChooseIdpDescription": "Изберете своя доставчик на идентичност, за да продължите", "orgAuthNoIdpConfigured": "Тази организация няма конфигурирани доставчици на идентичност. Можете да влезете с вашата Pangolin идентичност.", "orgAuthSignInWithPangolin": "Впишете се с Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Активирайте двуфакторното удостоверяване", "completeSecuritySteps": "Завършете стъпките за сигурност", "securitySettings": "Настройки за сигурност", - "securitySettingsDescription": "Конфигурирайте политиките за сигурност на вашата организация", + "securitySettingsDescription": "Конфигурирайте политики за сигурност за организацията", "requireTwoFactorForAllUsers": "Изисквайте двуфакторно удостоверяване за всички потребители", "requireTwoFactorDescription": "Когато е активирано, всички вътрешни потребители в организацията трябва да имат активирано двуфакторно удостоверяване, за да имат достъп до организацията.", "requireTwoFactorDisabledDescription": "Тази функция изисква валиден лиценз (Enterprise) или активен абонамент (SaaS).", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Корпоративно издание", "unlicensed": "Без лиценз", "beta": "Бета", - "manageClients": "Управление на клиенти", - "manageClientsDescription": "Клиентите са устройства, които могат да се свързват към вашите сайтове", + "manageUserDevices": "Потребителски устройства", + "manageUserDevicesDescription": "Прегледайте и управлявайте устройства, които потребителите използват за поверително свързване към ресурси", + "manageMachineClients": "Управлявайте машинни клиенти", + "manageMachineClientsDescription": "Създавайте и управлявайте клиенти, които сървърите и системите използват за поверително свързване към ресурси", + "clientsTableUserClients": "Потребител", + "clientsTableMachineClients": "Машина", "licenseTableValidUntil": "Валиден до", "saasLicenseKeysSettingsTitle": "Корпоративни лицензи", "saasLicenseKeysSettingsDescription": "Генериране и управление на корпоративни лицензионни ключове за самостоятелно хоствани инстанции на Pangolin", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "премахване", "sidebarEnableEnterpriseLicense": "Активиране на корпоративен лиценз", "cannotbeUndone": "Това не може да се отмени.", - "toConfirm": "за потвърждение", + "toConfirm": "за да потвърдите.", "deleteClientQuestion": "Сигурни ли сте, че искате да премахнете клиента от сайта и организацията?", "clientMessageRemove": "След като клиентът бъде премахнат, той вече няма да може да се свързва с сайта.", "sidebarLogs": "Логове", "request": "Изискване", + "requests": "Заявки", "logs": "Логове", "logsSettingsDescription": "Следете логовете, събрани от тази организация", "searchLogs": "Търсете в логовете...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Причина", "requestLogs": "Заявка за логове", + "requestAnalytics": "Анализи На Заявки", "host": "Хост", "location": "Местоположение", "actionLogs": "Дневници на действията", @@ -2029,6 +2094,7 @@ "logRetention": "Задържане на логове", "logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте", "requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация", + "requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация", "logRetentionRequestLabel": "Задържане на логове на заявки", "logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките", "logRetentionAccessLabel": "Задържане на логове за достъп", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 дни", "logRetention90Days": "90 дни", "logRetentionForever": "Завинаги", + "logRetentionEndOfFollowingYear": "Край на следващата година", "actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация", "accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация", "licenseRequiredToUse": "Необходим е лиценз Enterprise, за да се използва тази функция.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Предпочитайте универсален сертификат", "unverified": "Невалидиран", "domainSetting": "Настройки на домейните", - "domainSettingDescription": "Конфигурирайте настройките за вашия домейн", + "domainSettingDescription": "Конфигурирайте настройките за домейна", "preferWildcardCertDescription": "Опит за генериране на универсален сертификат (изисква правилно конфигуриран решавач на сертификати).", "recordName": "Име на запис", "auto": "Автоматично", @@ -2066,9 +2133,9 @@ "olmUpdateAvailableInfo": "Налична е актуализирана версия на Olm. Моля, актуализирайте до най-новата версия за най-добро преживяване.", "client": "Клиент", "proxyProtocol": "Настройки на прокси протокол", - "proxyProtocolDescription": "Конфигурирайте прокси протокол за запазване на IP адресите на клиентите за TCP/UDP услуги.", + "proxyProtocolDescription": "Конфигурирайте Proxy Protocol, за да запазите IP адресите на клиентите за TCP услуги.", "enableProxyProtocol": "Активирайте прокси протокола", - "proxyProtocolInfo": "Запазете IP адресите на клиентите за TCP/UDP бекенди", + "proxyProtocolInfo": "Запазете IP адресите на клиентите за TCP бекендове", "proxyProtocolVersion": "Версия на прокси протокола", "version1": "Версия 1 (Препоръчително)", "version2": "Версия 2", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Съобщението е изпратено!", "supportWillContact": "Ще се свържем с вас скоро!", "selectLogRetention": "Изберете съхранение на логовете", + "terms": "Термини", + "privacy": "Поверителност", + "security": "Сигурност", + "docs": "Документи", + "deviceActivation": "Активиране на устройство", + "deviceCodeInvalidFormat": "Кодът трябва да бъде 9 символа (напр. A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Невалиден или изтекъл код", + "deviceCodeVerifyFailed": "Неуспешна проверка на кода на устройството", + "signedInAs": "Вписан като", + "deviceCodeEnterPrompt": "Въведете кода, показан на устройството", + "continue": "Продължете", + "deviceUnknownLocation": "Неизвестно местоположение", + "deviceAuthorizationRequested": "Това разрешение беше заявено от {location} на {date}. Уверете се, че се доверявате на това устройство, тъй като то ще получи достъп до акаунта.", + "deviceLabel": "Устройство: {deviceName}", + "deviceWantsAccess": "иска да има достъп до вашия акаунт", + "deviceExistingAccess": "Съществуващ достъп:", + "deviceFullAccess": "Пълен достъп до вашия акаунт", + "deviceOrganizationsAccess": "Достъп до всички организации, до които има достъп акаунтът ви", + "deviceAuthorize": "Разрешете {applicationName}", + "deviceConnected": "Устройството е свързано!", + "deviceAuthorizedMessage": "Устройството е разрешено да има достъп до вашия акаунт.", + "pangolinCloud": "Pangolin Cloud", + "viewDevices": "Преглед на устройствата", + "viewDevicesDescription": "Управлявайте свързаните си устройства", + "noDevices": "Не са намерени устройства", + "dateCreated": "Дата на създаване", + "unnamedDevice": "Устройство без име", + "deviceQuestionRemove": "Сигурни ли сте, че искате да изтриете това устройство?", + "deviceMessageRemove": "Това действие не може да бъде отменено.", + "deviceDeleteConfirm": "Изтриване на устройство", + "deleteDevice": "Изтриване на устройство", + "errorLoadingDevices": "Грешка при зареждане на устройства", + "failedToLoadDevices": "Неуспешно зареждане на устройства", + "deviceDeleted": "Устройството е изтрито", + "deviceDeletedDescription": "Устройството бе успешно изтрито.", + "errorDeletingDevice": "Грешка при изтриване на устройството", + "failedToDeleteDevice": "Неуспешно изтриване на устройството", "showColumns": "Покажи колони", "hideColumns": "Скрий колони", "columnVisibility": "Видимост на колоните", @@ -2111,10 +2215,14 @@ "enableSelected": "Разреши избраните", "disableSelected": "Забрани избраните", "checkSelectedStatus": "Проверете състоянието на избраните", + "clients": "Клиенти", + "accessClientSelect": "Изберете машинни клиенти", + "resourceClientDescription": "Машинни клиенти, които могат да получат достъп до този ресурс", + "regenerate": "Генерирай", "credentials": "Удостоверения", "savecredentials": "Запазване на удостоверения", - "regeneratecredentials": "Прегенериране", - "regenerateCredentials": "Прегенериране и запазване на удостоверенията ви", + "regenerateCredentialsButton": "Генериране на нови удостоверителни данни", + "regenerateCredentials": "Генериране на нови удостоверителни данни", "generatedcredentials": "Прегенерирани удостоверения", "copyandsavethesecredentials": "Копирайте и запазете тези удостоверения", "copyandsavethesecredentialsdescription": "Тези удостоверения няма да бъдат показани отново след като напуснете тази страница. Запазете ги сигурно сега.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Удостоверенията бяха прегенерирани и успешно запазени.", "credentialsSaveError": "Грешка при запазването на удостоверенията", "credentialsSaveErrorDescription": "Възникна грешка при прегенерирането и запазването на удостоверенията.", - "regenerateCredentialsWarning": "Прегенерирането на удостоверения ще анулира предишните. Уверете се, че актуализирате всички конфигурации, които използват тези удостоверения.", + "regenerateCredentialsWarning": "Генерирането на нови удостоверителни данни ще направи предишните невалидни и ще причини прекъсване на връзката. Уверете се, че сте актуализирали всички конфигурации, които използват тези удостоверителни данни.", "confirm": "Потвърждаване", "regenerateCredentialsConfirmation": "Сигурни ли сте, че искате да прегенерирате удостоверенията?", "endpoint": "Крайна точка", "Id": "Идентификатор", "SecretKey": "Таен ключ", - "featureDisabledTooltip": "Тази функция е налична само в корпоративния пакет и изисква лиценз за използване.", "niceId": "Красив ID", "niceIdUpdated": "Красив ID е обновен", "niceIdUpdatedSuccessfully": "Красив ID е успешно обновен", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Възникна грешка при обновяването на Красив ID.", "niceIdCannotBeEmpty": "Красив ID не може да бъде празен", "enterIdentifier": "Въведете идентификатор", - "identifier": "Идентификатор" + "identifier": "Идентификатор", + "deviceLoginUseDifferentAccount": "Не сте вие? Използвайте друг акаунт.", + "deviceLoginDeviceRequestingAccessToAccount": "Устройство запитващо достъп до този акаунт.", + "noData": "Няма Данни", + "machineClients": "Машинни клиенти", + "install": "Инсталирай", + "run": "Изпълни", + "clientNameDescription": "Показваното име на клиента, което може да се промени по-късно.", + "clientAddress": "Клиентски адрес (Разширено)", + "setupFailedToFetchSubnet": "Неуспешно извличане на подмрежа по подразбиране", + "setupSubnetAdvanced": "Подмрежа (Разширено)", + "setupSubnetDescription": "Подмрежата за вътрешната мрежа на тази организация.", + "siteRegenerateAndDisconnect": "Генериране и прекъсване на връзката", + "siteRegenerateAndDisconnectConfirmation": "Сигурни ли сте, че искате да генерирате нови удостоверителни данни и да прекъснете тази връзка?", + "siteRegenerateAndDisconnectWarning": "Това ще генерира нови удостоверителни данни и незабавно ще прекъсне връзката. На сайта ще трябва да се рестартира с новите удостоверителни данни.", + "siteRegenerateCredentialsConfirmation": "Сигурни ли сте, че искате да генерирате новите удостоверителни данни за този сайт?", + "siteRegenerateCredentialsWarning": "Това ще генерира нови удостоверителни данни. Сайтът ще остане свързан, докато не го рестартирате ръчно и използвате новите удостоверителни данни.", + "clientRegenerateAndDisconnect": "Генериране и прекъсване на връзката", + "clientRegenerateAndDisconnectConfirmation": "Сигурни ли сте, че искате да генерирате нови удостоверителни данни и да прекъснете връзката на този клиент?", + "clientRegenerateAndDisconnectWarning": "Това ще генерира нови удостоверителни данни и незабавно ще прекъсне връзката на клиента. Клиентът ще трябва да се рестартира с новите удостоверителни данни.", + "clientRegenerateCredentialsConfirmation": "Сигурни ли сте, че искате да генерирате новите удостоверителни данни за този клиент?", + "clientRegenerateCredentialsWarning": "Това ще генерира нови удостоверителни данни. Клиентът ще остане свързан, докато не го рестартирате ръчно и използвате новите удостоверителни данни.", + "remoteExitNodeRegenerateAndDisconnect": "Генериране и прекъсване на връзката", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Сигурни ли сте, че искате да генерирате нови удостоверителни данни и да прекъснете връзката на този отдалечен възел?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Това ще генерира нови удостоверителни данни и незабавно ще прекъсне връзката на отдалечения възел. Отдалеченият възел ще трябва да се рестартира с новите удостоверителни данни.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Сигурни ли сте, че искате да генерирате новите удостоверителни данни за този отдалечен възел?", + "remoteExitNodeRegenerateCredentialsWarning": "Това ще генерира нови удостоверителни данни. Отдалеченият възел ще остане свързан, докато не го рестартирате ръчно и използвате новите удостоверителни данни.", + "agent": "Агент" } diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index ff1a6900..be3d2232 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -1,12 +1,12 @@ { - "setupCreate": "Vytvořte si organizaci, lokalitu a služby", + "setupCreate": "Vytvořte organizaci, stránku a zdroje", "setupNewOrg": "Nová organizace", "setupCreateOrg": "Vytvořit organizaci", "setupCreateResources": "Vytvořit zdroje", "setupOrgName": "Název organizace", - "orgDisplayName": "Toto je zobrazovaný název vaší organizace.", + "orgDisplayName": "Toto je zobrazený název organizace.", "orgId": "ID organizace", - "setupIdentifierMessage": "Toto je jedinečný identifikátor vaší organizace. Nemusí odpovídat názvu organizace.", + "setupIdentifierMessage": "Toto je jedinečný identifikátor organizace.", "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í.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Po odstranění webu již nebude přístupný. Všechny cíle spojené s webem budou také odstraněny.", "siteQuestionRemove": "Jste si jisti, že chcete odstranit tuto stránku z organizace?", "siteManageSites": "Správa lokalit", - "siteDescription": "Umožní připojení k vaší síti prostřednictvím zabezpečených tunelů", + "siteDescription": "Vytvořte a spravujte stránky pro povolení připojení k soukromým sítím", "siteCreate": "Vytvořit lokalitu", "siteCreateDescription2": "Postupujte podle níže uvedených kroků, abyste vytvořili a připojili novou lokalitu", - "siteCreateDescription": "Vytvořte novou lokalitu, abyste mohli začít připojovat služby", + "siteCreateDescription": "Vytvořit nový web pro zahájení připojování zdrojů", "close": "Zavřít", "siteErrorCreate": "Chyba při vytváření lokality", "siteErrorCreateKeyPair": "Nebyly nalezeny klíče nebo výchozí nastavení lokality", @@ -74,7 +74,7 @@ "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", + "WgConfigurationDescription": "K připojení k síti použijte následující konfiguraci", "operatingSystem": "Operační systém", "commands": "Příkazy", "recommended": "Doporučeno", @@ -87,32 +87,32 @@ "siteUpdated": "Lokalita upravena", "siteUpdatedDescription": "Lokalita byla upravena.", "siteGeneralDescription": "Upravte obecná nastavení pro tuto lokalitu", - "siteSettingDescription": "Upravte nastavení vaší lokality", + "siteSettingDescription": "Konfigurace nastavení na webu", "siteSetting": "Nastavení {siteName}", - "siteNewtTunnel": "Tunel Newt (doporučeno)", - "siteNewtTunnelDescription": "Nejjednodušší způsob, jak vytvořit vstupní bod do vaší sítě. Žádné další nastavení.", + "siteNewtTunnel": "Novinka (doporučeno)", + "siteNewtTunnelDescription": "Nejjednodušší způsob, jak vytvořit vstupní bod do jakékoli 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 místní zdroje. Žádný tunel. Dostupné pouze na vzdálených uzlech.", "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", + "siteTunnelDescription": "Určete, jak se chcete připojit k webu", + "siteNewtCredentials": "Pověření", + "siteNewtCredentialsDescription": "Takto se bude stránka autentizovat se serverem", + "siteCredentialsSave": "Uložit pověření", "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", + "shareDescription": "Vytvořit sdílitelné odkazy pro udělení dočasného nebo trvalého přístupu ke zdrojům proxy", "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.", + "shareTokenDescription": "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 klientovi na každé žádosti o ověřený přístup.", "accessToken": "Přístupový token", "usageExamples": "Příklady použití", "tokenId": "ID tokenu", @@ -121,7 +121,7 @@ "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": "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.", + "shareTokenSecurety": "Udržujte přístupový token v bezpečí. Nesdílejte jej ve veřejně přístupných oblastech nebo 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", @@ -144,8 +144,10 @@ "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", + "proxyResourceTitle": "Spravovat veřejné zdroje", + "proxyResourceDescription": "Vytváření a správa zdrojů, které jsou veřejně přístupné prostřednictvím webového prohlížeče", + "clientResourceTitle": "Spravovat soukromé zdroje", + "clientResourceDescription": "Vytváření a správa zdrojů, které jsou přístupné pouze prostřednictvím připojeného klienta", "resourcesSearch": "Prohledat zdroje...", "resourceAdd": "Přidat zdroj", "resourceErrorDelte": "Chyba při odstraňování zdroje", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Jakmile zdroj odstraníte, nebude dostupný. Všechny související služby a cíle budou také odstraněny.", "resourceQuestionRemove": "Jste si jisti, že chcete odstranit zdroj z organizace?", "resourceHTTP": "Zdroj HTTPS", - "resourceHTTPDescription": "Požadavky na proxy pro vaši aplikaci přes HTTPS pomocí subdomény nebo základní domény.", + "resourceHTTPDescription": "Požadavky na proxy pro aplikaci přes HTTPS pomocí subdomény nebo základní domény.", "resourceRaw": "Surový TCP/UDP zdroj", - "resourceRawDescription": "Požadavky na proxy pro vaši aplikaci přes TCP/UDP pomocí čísla portu.", + "resourceRawDescription": "Proxy požadavky na aplikaci přes TCP/UDP pomocí čísla portu. To funguje pouze v případě, že jsou stránky připojeny k uzlům.", "resourceCreate": "Vytvořit zdroj", "resourceCreateDescription": "Postupujte podle níže uvedených kroků, abyste vytvořili a připojili nový zdroj", "resourceSeeAll": "Zobrazit všechny zdroje", @@ -171,22 +173,22 @@ "noCountryFound": "Nebyla nalezena žádná země.", "siteSelectionDescription": "Tato lokalita poskytne připojení k cíli.", "resourceType": "Typ zdroje", - "resourceTypeDescription": "Určete, jak chcete přistupovat ke svému zdroji", + "resourceTypeDescription": "Určete, jak přistupovat ke zdroji", "resourceHTTPSSettings": "Nastavení HTTPS", - "resourceHTTPSSettingsDescription": "Nakonfigurujte, jak bude váš zdroj přístupný přes HTTPS", + "resourceHTTPSSettingsDescription": "Nakonfigurujte, jak bude dokument přístupný přes HTTPS", "domainType": "Typ domény", "subdomain": "Subdoména", "baseDomain": "Základní doména", - "subdomnainDescription": "Subdoména, kde bude váš zdroj přístupný.", + "subdomnainDescription": "Subdoména, kde bude zdroj přístupný.", "resourceRawSettings": "Nastavení TCP/UDP", - "resourceRawSettingsDescription": "Nakonfigurujte, jak bude váš dokument přístupný přes TCP/UDP. Mapováte zdroj na port na serveru Pangolin, takže můžete přistupovat ke zdroji ze serveru-veřejné ip:mapped-port.", + "resourceRawSettingsDescription": "Nakonfigurujte, jak bude dokument přístupný přes TCP/UDP", "protocol": "Protokol", "protocolSelect": "Vybrat protokol", "resourcePortNumber": "Číslo portu", "resourcePortNumberDescription": "Externí port k požadavkům proxy serveru.", "cancel": "Zrušit", "resourceConfig": "Konfigurační snippety", - "resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační snippety pro nastavení TCP/UDP zdroje", + "resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační textové bloky pro nastavení TCP/UDP zdroje", "resourceAddEntrypoints": "Traefik: Přidat vstupní body", "resourceExposePorts": "Gerbil: Expose Ports in Docker Compose", "resourceLearnRaw": "Naučte se konfigurovat zdroje TCP/UDP", @@ -202,14 +204,14 @@ "proxy": "Proxy server", "internal": "Interní", "rules": "Pravidla", - "resourceSettingDescription": "Konfigurace nastavení na vašem zdroji", + "resourceSettingDescription": "Konfigurace nastavení na zdroji", "resourceSetting": "Nastavení {resourceName}", - "alwaysAllow": "Vždy povolit", - "alwaysDeny": "Vždy zakázat", + "alwaysAllow": "Obejít Auth", + "alwaysDeny": "Blokovat přístup", "passToAuth": "Předat k ověření", - "orgSettingsDescription": "Konfigurace obecných nastavení vaší organizace", + "orgSettingsDescription": "Konfigurace nastavení organizace", "orgGeneralSettings": "Nastavení organizace", - "orgGeneralSettingsDescription": "Spravujte údaje a konfiguraci vaší organizace", + "orgGeneralSettingsDescription": "Spravovat podrobnosti a konfiguraci organizace", "saveGeneralSettings": "Uložit obecné nastavení", "saveSettings": "Uložit nastavení", "orgDangerZone": "Nebezpečná zóna", @@ -232,7 +234,7 @@ "orgMissing": "Chybí ID organizace", "orgMissingMessage": "Nelze obnovit pozvánku bez ID organizace.", "accessUsersManage": "Spravovat uživatele", - "accessUsersDescription": "Pozvěte uživatele a přidejte je do rolí pro správu přístupu do vaší organizace", + "accessUsersDescription": "Pozvat a spravovat uživatele s přístupem k této organizaci", "accessUsersSearch": "Hledat uživatele...", "accessUserCreate": "Vytvořit uživatele", "accessUserRemove": "Odstranit uživatele", @@ -241,13 +243,13 @@ "role": "Role", "nameRequired": "Název je povinný", "accessRolesManage": "Spravovat role", - "accessRolesDescription": "Konfigurace rolí pro správu přístupu do vaší organizace", + "accessRolesDescription": "Vytvořit a spravovat role pro uživatele v organizaci", "accessRolesSearch": "Hledat role...", "accessRolesAdd": "Přidat roli", "accessRoleDelete": "Odstranit roli", "description": "L 343, 22.12.2009, s. 1).", "inviteTitle": "Otevřít pozvánky", - "inviteDescription": "Spravujte své pozvánky ostatním uživatelům", + "inviteDescription": "Spravovat pozvánky pro ostatní uživatele do organizace", "inviteSearch": "Hledat pozvánky...", "minutes": "Zápis z jednání", "hours": "Hodiny", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Chyba při vytváření API klíče", "apiKeysErrorSetPermission": "Chyba nastavení oprávnění", "apiKeysCreate": "Generovat API klíč", - "apiKeysCreateDescription": "Vygenerovat nový API klíč pro vaši organizaci", + "apiKeysCreateDescription": "Vygenerovat nový API klíč pro organizaci", "apiKeysGeneralSettings": "Práva", "apiKeysGeneralSettingsDescription": "Určete, co může tento API klíč udělat", - "apiKeysList": "Váš API klíč", - "apiKeysSave": "Uložit váš API klíč", + "apiKeysList": "Nový API klíč", + "apiKeysSave": "Uložit API klíč", "apiKeysSaveDescription": "Toto nastavení uvidíte pouze jednou. Ujistěte se, že jej zkopírujete na bezpečné místo.", - "apiKeysInfo": "Váš API klíč je:", + "apiKeysInfo": "API klíč je:", "apiKeysConfirmCopy": "Kopíroval jsem API klíč", "generate": "Generovat", "done": "Hotovo", @@ -424,7 +426,7 @@ "userCreated": "Uživatel byl vytvořen", "userCreatedDescription": "Uživatel byl úspěšně vytvořen.", "userTypeInternal": "Interní uživatel", - "userTypeInternalDescription": "Pozvěte uživatele do vaší organizace přímo.", + "userTypeInternalDescription": "Pozvěte uživatele do organizace přímo.", "userTypeExternal": "Externí uživatel", "userTypeExternalDescription": "Vytvořte uživatele s externím poskytovatelem identity.", "accessUserCreateDescription": "Postupujte podle níže uvedených kroků pro vytvoření nového uživatele", @@ -436,6 +438,16 @@ "inviteEmailSent": "Poslat uživateli pozvánku", "inviteValid": "Platné pro", "selectDuration": "Vyberte dobu trvání", + "selectResource": "Vybrat dokument", + "filterByResource": "Filtrovat podle zdroje", + "resetFilters": "Resetovat filtry", + "totalBlocked": "Požadavky blokovány Pangolinem", + "totalRequests": "Celkem požadavků", + "requestsByCountry": "Žádosti podle země", + "requestsByDay": "Žádosti podle dne", + "blocked": "Blokované", + "allowed": "Povoleno", + "topCountries": "Nejlepší země", "accessRoleSelect": "Vybrat roli", "inviteEmailSentDescription": "Uživateli byl odeslán e-mail s odkazem pro přístup níže. Pro přijetí pozvánky musí mít přístup k odkazu.", "inviteSentDescription": "Uživatel byl pozván. Pro přijetí pozvánky musí mít přístup na níže uvedený odkaz.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Uložit kontroly přístupu", "roles": "Role", "accessUsersRoles": "Spravovat uživatele a role", - "accessUsersRolesDescription": "Pozvěte uživatele a přidejte je do rolí pro správu přístupu do vaší organizace", + "accessUsersRolesDescription": "Pozvěte uživatele a přidejte je do rolí pro správu přístupu k organizaci", "key": "Klíč", "createdAt": "Vytvořeno v", "proxyErrorInvalidHeader": "Neplatná hodnota hlavičky hostitele. Použijte formát názvu domény, nebo uložte prázdné pro zrušení vlastního hlavičky hostitele.", "proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.", "proxyEnableSSL": "Povolit SSL", - "proxyEnableSSLDescription": "Povolit šifrování SSL/TLS pro zabezpečená HTTPS připojení k vašim cílům.", + "proxyEnableSSLDescription": "Povolit šifrování SSL/TLS pro zabezpečená připojení HTTPS k cílům.", "target": "Target", "configureTarget": "Konfigurace cílů", "targetErrorFetch": "Nepodařilo se načíst cíle", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Nepodařilo se aktualizovat cíle", "targetsErrorUpdateDescription": "Došlo k chybě při aktualizaci cílů", "targetTlsUpdate": "Nastavení TLS aktualizováno", - "targetTlsUpdateDescription": "Vaše nastavení TLS bylo úspěšně aktualizováno", + "targetTlsUpdateDescription": "TLS nastavení bylo úspěšně aktualizováno", "targetErrorTlsUpdate": "Aktualizace nastavení TLS se nezdařila", "targetErrorTlsUpdateDescription": "Došlo k chybě při aktualizaci nastavení TLS", "proxyUpdated": "Nastavení proxy bylo aktualizováno", - "proxyUpdatedDescription": "Vaše nastavení proxy bylo úspěšně aktualizováno", + "proxyUpdatedDescription": "Nastavení proxy bylo úspěšně aktualizováno", "proxyErrorUpdate": "Aktualizace nastavení proxy se nezdařila", "proxyErrorUpdateDescription": "Došlo k chybě při aktualizaci nastavení proxy", - "targetAddr": "IP / Hostname", + "targetAddr": "Hostitel", "targetPort": "Přístav", "targetProtocol": "Protokol", "targetTlsSettings": "Nastavení bezpečného připojení", - "targetTlsSettingsDescription": "Konfigurace nastavení SSL/TLS pro váš dokument", + "targetTlsSettingsDescription": "Nastavení SSL/TLS pro zdroj", "targetTlsSettingsAdvanced": "Pokročilé nastavení TLS", "targetTlsSni": "Název serveru TLS", "targetTlsSniDescription": "Název serveru TLS pro použití v SNI. Ponechte prázdné pro použití výchozího nastavení.", "targetTlsSubmit": "Uložit nastavení", "targets": "Konfigurace cílů", - "targetsDescription": "Nastavte cíle pro směrování provozu do záložních služeb", + "targetsDescription": "Nastavte cíle pro trasu provozu do záložních služeb", "targetStickySessions": "Povolit Rychlé relace", "targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.", "methodSelect": "Vyberte metodu", "targetSubmit": "Add Target", - "targetNoOne": "Tento zdroj nemá žádné cíle. Přidejte cíl pro konfiguraci kam poslat žádosti na vaši backend.", + "targetNoOne": "Tento zdroj nemá žádné cíle. Přidejte cíl pro konfiguraci kam poslat žádosti na backend.", "targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.", "targetsSubmit": "Uložit cíle", "addTarget": "Add Target", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Cíl byl úspěšně vytvořen", "targetErrorCreate": "Nepodařilo se vytvořit cíl", "targetErrorCreateDescription": "Došlo k chybě při vytváření cíle", + "tlsServerName": "Název serveru TLS", + "tlsServerNameDescription": "Název serveru TLS pro SNI", "save": "Uložit", "proxyAdditional": "Další nastavení proxy", - "proxyAdditionalDescription": "Konfigurovat nastavení proxy zpracování vašeho zdroje", + "proxyAdditionalDescription": "Konfigurovat nastavení proxy", "proxyCustomHeader": "Vlastní hlavička hostitele", "proxyCustomHeaderDescription": "Hlavička hostitele bude nastavena při proxování požadavků. Nechte prázdné pro použití výchozího nastavení.", "proxyAdditionalSubmit": "Uložit nastavení proxy", @@ -558,7 +572,7 @@ "rulesMatchType": "Typ shody", "value": "Hodnota", "rulesAbout": "O pravidlech", - "rulesAboutDescription": "Pravidla vám umožňují kontrolovat přístup k vašemu zdroji na základě sady kritérií. Můžete vytvořit pravidla pro povolení nebo zamítnutí přístupu na základě IP adresy nebo cesty URL.", + "rulesAboutDescription": "Pravidla vám umožňují kontrolovat přístup ke zdrojům na základě souboru kritérií. Můžete vytvořit pravidla pro povolení nebo zamítnutí přístupu na základě IP adresy nebo cesty URL.", "rulesActions": "Akce", "rulesActionAlwaysAllow": "Vždy Povolit: Obejít všechny metody ověřování", "rulesActionAlwaysDeny": "Vždy odepří: Zablokovat všechny požadavky; nelze se pokusit o ověření", @@ -570,7 +584,7 @@ "rulesEnable": "Povolit pravidla", "rulesEnableDescription": "Povolit nebo zakázat hodnocení pravidel pro tento zdroj", "rulesResource": "Konfigurace pravidel zdroje", - "rulesResourceDescription": "Konfigurace pravidel pro kontrolu přístupu k vašemu zdroji", + "rulesResourceDescription": "Nastavit pravidla pro kontrolu přístupu ke zdroji", "ruleSubmit": "Přidat pravidlo", "rulesNoOne": "Žádná pravidla. Přidejte pravidlo pomocí formuláře.", "rulesOrder": "Pravidla jsou hodnocena podle priority vzestupně.", @@ -586,7 +600,7 @@ "none": "Nic", "unknown": "Neznámý", "resources": "Zdroje", - "resourcesDescription": "Zdroje jsou proxy aplikací běžících na vaší soukromé síti. Vytvořte zdroj pro jakoukoli HTTP/HTTPS nebo nakreslete TCP/UDP službu na vaší soukromé síti. Každý zdroj musí být připojen k webu pro povolení soukromého, zabezpečeného připojení pomocí šifrovaného tunelu WireGuard.", + "resourcesDescription": "Zdroje jsou proxy aplikací běžících na soukromé síti. Vytvořte zdroj pro jakoukoli HTTP/HTTPS nebo nakreslete TCP/UDP službu v soukromé síti. Každý zdroj musí být připojen k webu pro povolení soukromého, zabezpečeného připojení pomocí šifrovaného tunelu WireGuard.", "resourcesWireGuardConnect": "Bezpečné připojení s šifrováním WireGuard", "resourcesMultipleAuthenticationMethods": "Konfigurace vícenásobných metod ověřování", "resourcesUsersRolesAccess": "Kontrola přístupu na základě uživatelů a rolí", @@ -597,7 +611,7 @@ "resourceSelect": "Vyberte zdroj", "shareLinks": "Sdílet odkazy", "share": "Sdílené odkazy", - "shareDescription2": "Vytvořte sdílitelné odkazy na vaše zdroje. Odkazy poskytují dočasný nebo neomezený přístup k vašemu zdroji. Můžete nakonfigurovat dobu vypršení platnosti odkazu při jeho vytvoření.", + "shareDescription2": "Vytvořte sdílitelné odkazy na zdroje. Odkazy poskytují dočasný nebo neomezený přístup k vašemu zdroji. Můžete nakonfigurovat dobu vypršení platnosti odkazu při jeho vytvoření.", "shareEasyCreate": "Snadné vytváření a sdílení", "shareConfigurableExpirationDuration": "Konfigurovatelná doba vypršení platnosti", "shareSecureAndRevocable": "Bezpečné a odvolatelné", @@ -607,19 +621,19 @@ "unknownCommand": "Neznámý příkaz", "newtErrorFetchReleases": "Nepodařilo se načíst informace o vydání: {err}", "newtErrorFetchLatest": "Chyba při načítání nejnovější verze: {err}", - "newtEndpoint": "Newt Endpoint", - "newtId": "Newt ID", - "newtSecretKey": "Tajný klíč novinky", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Tajný klíč", "architecture": "Architektura", "sites": "Stránky", - "siteWgAnyClients": "K připojení použijte jakéhokoli klienta WireGuard. Budete muset řešit své interní zdroje pomocí klientské IP adresy.", + "siteWgAnyClients": "K připojení použijte jakéhokoli klienta WireGuard. Budete muset řešit interní zdroje pomocí klientské IP adresy.", "siteWgCompatibleAllClients": "Kompatibilní se všemi klienty aplikace WireGuard", "siteWgManualConfigurationRequired": "Je vyžadována ruční konfigurace", "userErrorNotAdminOrOwner": "Uživatel není administrátor nebo vlastník", "pangolinSettings": "Nastavení - Pangolin", "accessRoleYour": "Vaše role:", - "accessRoleSelect2": "Vyberte roli", - "accessUserSelect": "Vyberte uživatele", + "accessRoleSelect2": "Vybrat role", + "accessUserSelect": "Vybrat uživatele", "otpEmailEnter": "Zadejte e-mail", "otpEmailEnterDescription": "Stisknutím klávesy Enter přidáte e-mail po zadání do vstupního pole.", "otpEmailErrorInvalid": "Neplatná e-mailová adresa. Wildcard (*) musí být celá místní část.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Nastavit anonymní kód", "resourcePincodeSetupTitleDescription": "Nastavit pincode pro ochranu tohoto zdroje", "resourceRoleDescription": "Administrátoři mají vždy přístup k tomuto zdroji.", - "resourceUsersRoles": "Uživatelé a role", + "resourceUsersRoles": "Kontrola přístupu", "resourceUsersRolesDescription": "Nastavení, kteří uživatelé a role mohou navštívit tento zdroj", "resourceUsersRolesSubmit": "Uložit uživatele a role", "resourceWhitelistSave": "Úspěšně uloženo", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Přenos zdroje", "siteDestination": "Cílová stránka", "searchSites": "Hledat lokality", + "countries": "Země", "accessRoleCreate": "Vytvořit roli", "accessRoleCreateDescription": "Vytvořte novou roli pro seskupení uživatelů a spravujte jejich oprávnění.", "accessRoleCreateSubmit": "Vytvořit roli", @@ -768,7 +783,7 @@ "idpClientId": "ID klienta", "idpClientIdDescription": "Klientské ID OAuth2 od poskytovatele identity", "idpClientSecret": "Tajný klíč klienta", - "idpClientSecretDescription": "OAuth2 klíč klienta od poskytovatele identity", + "idpClientSecretDescription": "OAuth2 heslo klienta od poskytovatele identity", "idpAuthUrl": "Autorizační URL", "idpAuthUrlDescription": "Koncový koncový bod ověření OAuth2", "idpTokenUrl": "URL tokenu", @@ -791,7 +806,7 @@ "idpSubmit": "Vytvořit poskytovatele identity", "orgPolicies": "Zásady organizace", "idpSettings": "Nastavení {idpName}", - "idpCreateSettingsDescription": "Konfigurace nastavení pro poskytovatele identity", + "idpCreateSettingsDescription": "Konfigurace nastavení poskytovatele identity", "roleMapping": "Mapování rolí", "orgMapping": "Mapování organizace", "orgPoliciesSearch": "Hledat v zásadách organizace...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Poskytovatel identity byl úspěšně aktualizován", "redirectUrl": "Přesměrovat URL", "redirectUrlAbout": "O přesměrování URL", - "redirectUrlAboutDescription": "Toto je URL, na kterou budou uživatelé po ověření přesměrováni. Tuto URL je třeba nakonfigurovat v nastavení poskytovatele identity.", + "redirectUrlAboutDescription": "Toto je URL, na kterou budou uživatelé po ověření přesměrováni. Tuto URL je třeba nastavit v nastavení poskytovatele identity.", "pangolinAuth": "Auth - Pangolin", "verificationCodeLengthRequirements": "Váš ověřovací kód musí mít 8 znaků.", "errorOccurred": "Došlo k chybě", @@ -909,6 +924,10 @@ "passwordResetSent": "Na tuto e-mailovou adresu zašleme kód pro obnovení hesla.", "passwordResetCode": "Reset Code", "passwordResetCodeDescription": "Zkontrolujte svůj e-mail pro kód pro obnovení.", + "generatePasswordResetCode": "Vygenerovat kód pro obnovení hesla", + "passwordResetCodeGenerated": "Kód pro obnovení hesla byl vytvořen", + "passwordResetCodeGeneratedDescription": "Sdílejte tento kód s uživatelem. Mohou jej použít k obnovení hesla.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nové heslo", "passwordNewConfirm": "Potvrdit nové heslo", "changePassword": "Změnit heslo", @@ -926,6 +945,9 @@ "pincodeAuth": "Ověřovací kód", "pincodeSubmit2": "Odeslat kód", "passwordResetSubmit": "Žádost o obnovení", + "passwordResetAlreadyHaveCode": "Zadejte kód pro obnovení hesla", + "passwordResetSmtpRequired": "Obraťte se na správce", + "passwordResetSmtpRequiredDescription": "Pro obnovení hesla je vyžadován kód pro obnovení hesla. Kontaktujte prosím svého administrátora.", "passwordBack": "Zpět na heslo", "loginBack": "Přejít zpět na přihlášení", "signup": "Zaregistrovat se", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Seznam zdrojů webu", "actionUpdateSiteResource": "Aktualizovat dokument webu", "actionListInvitations": "Seznam pozvánek", + "actionExportLogs": "Exportovat protokoly", + "actionViewLogs": "Zobrazit logy", "noneSelected": "Není vybráno", "orgNotFound2": "Nebyly nalezeny žádné organizace.", "searchProgress": "Hledat...", "create": "Vytvořit", "orgs": "Organizace", "loginError": "Při přihlášení došlo k chybě", + "loginRequiredForDevice": "Pro ověření vašeho zařízení je nutné se přihlásit.", "passwordForgot": "Zapomněli jste heslo?", "otpAuth": "Dvoufaktorové ověření", "otpAuthDescription": "Zadejte kód z vaší autentizační aplikace nebo jeden z vlastních záložních kódů.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Domů", "sidebarSites": "Stránky", "sidebarResources": "Zdroje", + "sidebarProxyResources": "Veřejnost", + "sidebarClientResources": "Soukromé", "sidebarAccessControl": "Kontrola přístupu", + "sidebarLogsAndAnalytics": "Logy & Analytika", "sidebarUsers": "Uživatelé", + "sidebarAdmin": "Admin", "sidebarInvitations": "Pozvánky", "sidebarRoles": "Role", - "sidebarShareableLinks": "Sdílené odkazy", + "sidebarShareableLinks": "Odkazy", "sidebarApiKeys": "API klíče", "sidebarSettings": "Nastavení", "sidebarAllUsers": "Všichni uživatelé", "sidebarIdentityProviders": "Poskytovatelé identity", "sidebarLicense": "Licence", "sidebarClients": "Klienti", + "sidebarUserDevices": "Uživatelé", + "sidebarMachineClients": "Stroje a přístroje", "sidebarDomains": "Domény", + "sidebarGeneral": "Obecná ustanovení", + "sidebarLogAndAnalytics": "Log & Analytics", "sidebarBluePrints": "Plány", + "sidebarOrganization": "Organizace", + "sidebarLogsAnalytics": "Analytici", "blueprints": "Plány", "blueprintsDescription": "Použít deklarativní konfigurace a zobrazit předchozí běhy", "blueprintAdd": "Přidat plán", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Podívejte se na výsledek použitého plánu a případné chyby, které se vyskytly", "blueprintInfo": "Informace o plánu", "message": "Zpráva", - "blueprintContentsDescription": "Definujte obsah YAML popisující vaši infrastrukturu", + "blueprintContentsDescription": "Definujte obsah YAML popisující infrastrukturu", "blueprintErrorCreateDescription": "Při aplikaci plánu došlo k chybě", "blueprintErrorCreate": "Chyba při vytváření plánu", "searchBlueprintProgress": "Hledat plány...", @@ -1230,15 +1265,15 @@ "loading": "Načítání", "restart": "Restartovat", "domains": "Domény", - "domainsDescription": "Spravovat domény pro vaši organizaci", + "domainsDescription": "Vytvořit a spravovat domény dostupné v organizaci", "domainsSearch": "Hledat domény...", "domainAdd": "Přidat doménu", - "domainAddDescription": "Registrujte novou doménu ve vaší organizaci", + "domainAddDescription": "Registrovat novou doménu s organizací", "domainCreate": "Vytvořit doménu", "domainCreatedDescription": "Doména byla úspěšně vytvořena", "domainDeletedDescription": "Doména byla úspěšně odstraněna", - "domainQuestionRemove": "Opravdu chcete odstranit doménu z vašeho účtu?", - "domainMessageRemove": "Po odstranění domény již nebude přiřazena k vašemu účtu.", + "domainQuestionRemove": "Jste si jisti, že chcete odstranit doménu?", + "domainMessageRemove": "Po odstranění domény již nebude přiřazena k organizaci.", "domainConfirmDelete": "Potvrdit odstranění domény", "domainDelete": "Odstranit doménu", "domain": "Doména", @@ -1257,7 +1292,7 @@ "pending": "Nevyřízeno", "sidebarBilling": "Fakturace", "billing": "Fakturace", - "orgBillingDescription": "Spravujte své fakturační údaje a předplatné", + "orgBillingDescription": "Spravovat fakturační informace a předplatné", "github": "GitHub", "pangolinHosted": "Pangolin hostovaný", "fossorial": "Fossorální", @@ -1285,7 +1320,7 @@ "productUpdateTitle": "Aktualizace produktu", "productUpdateEmpty": "Žádné aktualizace", "dismissAll": "Odmítnout vše", - "pangolinUpdateAvailable": "K dispozici je nová verze", + "pangolinUpdateAvailable": "Dostupná aktualizace", "pangolinUpdateAvailableInfo": "Verze {version} je připravena k instalaci", "pangolinUpdateAvailableReleaseNotes": "Zobrazit poznámky k vydání", "newtUpdateAvailable": "Dostupná aktualizace", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Kontrola dostupnosti...", - "domainPickerNoMatchingDomains": "Nebyly nalezeny žádné odpovídající domény. Zkuste jinou doménu nebo zkontrolujte nastavení domény vaší organizace.", + "domainPickerNoMatchingDomains": "Nebyly nalezeny žádné odpovídající domény. Zkuste jinou doménu nebo zkontrolujte nastavení domény organizace.", "domainPickerOrganizationDomains": "Domény organizace", "domainPickerProvidedDomains": "Poskytnuté domény", "domainPickerSubdomain": "Subdoména: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Upravit předplatné", "billingStartSubscription": "Začít předplatné", "billingRecurringCharge": "Opakované nabití", - "billingManageSubscriptionSettings": "Spravovat nastavení a nastavení předplatného", + "billingManageSubscriptionSettings": "Správa nastavení předplatného a předvoleb", "billingNoActiveSubscription": "Nemáte aktivní předplatné. Začněte předplatné, abyste zvýšili omezení používání.", "billingFailedToLoadSubscription": "Nepodařilo se načíst odběr", "billingFailedToLoadUsage": "Nepodařilo se načíst využití", @@ -1345,9 +1380,9 @@ "billingPortalError": "Chyba portálu", "billingDataUsageInfo": "Pokud jste připojeni k cloudu, jsou vám účtována všechna data přenášená prostřednictvím zabezpečených tunelů. To zahrnuje příchozí i odchozí provoz na všech vašich stránkách. Jakmile dosáhnete svého limitu, vaše stránky se odpojí, dokud neaktualizujete svůj tarif nebo nezmenšíte jeho používání. Data nejsou nabírána při používání uzlů.", "billingOnlineTimeInfo": "Platíte na základě toho, jak dlouho budou vaše stránky připojeny k cloudu. Například, 44,640 minut se rovná jedné stránce 24/7 po celý měsíc. Jakmile dosáhnete svého limitu, vaše stránky se odpojí, dokud neaktualizujete svůj tarif nebo nezkrátíte jeho používání. Čas není vybírán při používání uzlů.", - "billingUsersInfo": "Obdrželi jste platbu za každého uživatele ve vaší organizaci. Fakturace je počítána denně na základě počtu aktivních uživatelských účtů ve vašem org.", - "billingDomainInfo": "Platba je účtována za každou doménu ve vaší organizaci. Fakturace je počítána denně na základě počtu aktivních doménových účtů na Vašem org.", - "billingRemoteExitNodesInfo": "Za každý spravovaný uzel ve vaší organizaci se vám účtuje denně. Fakturace je počítána na základě počtu aktivních spravovaných uzlů ve vašem org.", + "billingUsersInfo": "Každý uživatel v organizaci je účtován denně. Fakturace je počítána na základě počtu aktivních uživatelských účtů na Vašem org.", + "billingDomainInfo": "Objednávka je účtována za každou doménu v organizaci. Fakturace je počítána denně na základě počtu aktivních doménových účtů na Vašem org.", + "billingRemoteExitNodesInfo": "Za každý spravovaný uzel v organizaci se vám účtuje denně. Fakturace je vypočítávána na základě počtu aktivních spravovaných uzlů ve vašem org.", "domainNotFound": "Doména nenalezena", "domainNotFoundDescription": "Tento dokument je zakázán, protože doména již neexistuje náš systém. Nastavte prosím novou doménu pro tento dokument.", "failed": "Selhalo", @@ -1430,29 +1465,32 @@ "and": "a", "privacyPolicy": "zásady ochrany osobních údajů" }, + "signUpMarketing": { + "keepMeInTheLoop": "Udržujte mě ve smyčce s novinkami, aktualizacemi a novými funkcemi e-mailem." + }, "siteRequired": "Stránka je povinná.", "olmTunnel": "Starý tunel", "olmTunnelDescription": "Použít Olm pro připojení klienta", "errorCreatingClient": "Chyba při vytváření klienta", "clientDefaultsNotFound": "Výchozí hodnoty klienta nebyly nalezeny", "createClient": "Vytvořit klienta", - "createClientDescription": "Vytvořte nového klienta pro připojení k vašim stránkám", + "createClientDescription": "Vytvořte nového klienta pro přístup k soukromým zdrojům", "seeAllClients": "Zobrazit všechny klienty", "clientInformation": "Informace o klientovi", "clientNamePlaceholder": "Název klienta", "address": "Adresa", "subnetPlaceholder": "Podsíť", - "addressDescription": "Adresa, kterou bude tento klient používat pro připojení", + "addressDescription": "Interní adresa klienta. Musí spadat do podsítě organizace.", "selectSites": "Vyberte stránky", "sitesDescription": "Klient bude mít připojení k vybraným webům", "clientInstallOlm": "Nainstalovat Olm", "clientInstallOlmDescription": "Stáhněte si Olm běžící ve vašem systému", - "clientOlmCredentials": "Olm pověření", - "clientOlmCredentialsDescription": "Tímto způsobem se bude Olm autentizovat se serverem", - "olmEndpoint": "Olm koncový bod", - "olmId": "Olm ID", - "olmSecretKey": "Olm tajný klíč", - "clientCredentialsSave": "Uložit přihlašovací údaje", + "clientOlmCredentials": "Pověření", + "clientOlmCredentialsDescription": "Tímto způsobem bude klient autentizovat se serverem", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Tajný klíč", + "clientCredentialsSave": "Uložit pověření", "clientCredentialsSaveDescription": "Toto nastavení uvidíte pouze jednou. Ujistěte se, že jej zkopírujete na bezpečné místo.", "generalSettingsDescription": "Konfigurace obecných nastavení pro tohoto klienta", "clientUpdated": "Klient byl aktualizován", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Došlo k chybě při načítání stránek.", "olmErrorFetchReleases": "Při načítání vydání Olm došlo k chybě.", "olmErrorFetchLatest": "Došlo k chybě při načítání nejnovější verze Olm.", - "remoteSubnets": "Vzdálené podsítě", "enterCidrRange": "Zadejte rozsah CIDR", - "remoteSubnetsDescription": "Přidejte CIDR rozsahy, které mohou být přístupné z tohoto webu dálkově pomocí klientů. Použijte formát jako 10.0.0.0/24. Toto platí pro připojení klienta VPN.", "resourceEnableProxy": "Povolit veřejné proxy", "resourceEnableProxyDescription": "Povolit veřejné proxying pro tento zdroj. To umožňuje přístup ke zdrojům mimo síť prostřednictvím cloudu na otevřeném portu. Vyžaduje nastavení Traefik.", "externalProxyEnabled": "Externí proxy povolen", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Sledujte zdraví tohoto cíle. V případě potřeby můžete sledovat jiný cílový bod, než je cíl.", "healthScheme": "Způsob", "healthSelectScheme": "Vybrat metodu", + "healthCheckPortInvalid": "Přístav kontroly stavu musí být mezi 1 a 65535", "healthCheckPath": "Cesta", "healthHostname": "IP / Hostitel", "healthPort": "Přístav", "healthCheckPathDescription": "Cesta ke kontrole zdravotního stavu.", - "healthyIntervalSeconds": "Interval zdraví", - "unhealthyIntervalSeconds": "Nezdravý interval", + "healthyIntervalSeconds": "Zdravý interval (sek)", + "unhealthyIntervalSeconds": "Nezdravý interval (sek)", "IntervalSeconds": "Interval zdraví", - "timeoutSeconds": "Časový limit", + "timeoutSeconds": "Časový limit (sek)", "timeIsInSeconds": "Čas je v sekundách", "retryAttempts": "Opakovat pokusy", "expectedResponseCodes": "Očekávané kódy odezvy", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Upravit doménu", "siteName": "Název webu", "proxyPort": "Přístav", - "resourcesTableProxyResources": "Zdroje proxy", - "resourcesTableClientResources": "Zdroje klienta", + "resourcesTableProxyResources": "Veřejnost", + "resourcesTableClientResources": "Soukromé", "resourcesTableNoProxyResourcesFound": "Nebyly nalezeny žádné zdroje proxy", "resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.", "resourcesTableDestination": "Místo určení", - "resourcesTableTheseResourcesForUseWith": "Tyto zdroje jsou určeny pro použití s", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Klienti", "resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.", "resourcesTableNoTargets": "Žádné cíle", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Offline", "resourcesTableUnknown": "Neznámý", "resourcesTableNotMonitored": "Není sledováno", - "editInternalResourceDialogEditClientResource": "Upravit klientský dokument", - "editInternalResourceDialogUpdateResourceProperties": "Aktualizujte vlastnosti zdroje a cílovou konfiguraci pro {resourceName}.", + "editInternalResourceDialogEditClientResource": "Upravit soukromý dokument", + "editInternalResourceDialogUpdateResourceProperties": "Aktualizovat konfiguraci zdroje a ovládací prvky přístupu pro {resourceName}", "editInternalResourceDialogResourceProperties": "Vlastnosti zdroje", "editInternalResourceDialogName": "Jméno", "editInternalResourceDialogProtocol": "Protokol", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Neplatný formát IP adresy", "editInternalResourceDialogDestinationPortMin": "Cílový přístav musí být alespoň 1", "editInternalResourceDialogDestinationPortMax": "Cílový přístav musí být nižší než 65536", + "editInternalResourceDialogPortModeRequired": "Protokol, proxy port a cílový port jsou vyžadovány pro režim portu", + "editInternalResourceDialogMode": "Režim", + "editInternalResourceDialogModePort": "Přístav", + "editInternalResourceDialogModeHost": "Hostitel", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Místo určení", + "editInternalResourceDialogDestinationHostDescription": "IP adresa nebo název hostitele zdroje v síti webu.", + "editInternalResourceDialogDestinationIPDescription": "IP nebo název hostitele zdroje v síti webu.", + "editInternalResourceDialogDestinationCidrDescription": "Rozsah zdrojů CIDR v síti webu.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.", "createInternalResourceDialogNoSitesAvailable": "Nejsou k dispozici žádné weby", "createInternalResourceDialogNoSitesAvailableDescription": "Musíte mít alespoň jeden Newt web s podsítí nakonfigurovanou pro vytvoření vnitřních zdrojů.", "createInternalResourceDialogClose": "Zavřít", - "createInternalResourceDialogCreateClientResource": "Vytvořit klientský dokument", - "createInternalResourceDialogCreateClientResourceDescription": "Vytvořte nový zdroj, který bude přístupný klientům připojeným k vybranému webu.", + "createInternalResourceDialogCreateClientResource": "Vytvořit soukromý zdroj", + "createInternalResourceDialogCreateClientResourceDescription": "Vytvořte nový zdroj, který bude přístupný pouze klientům připojeným k organizaci", "createInternalResourceDialogResourceProperties": "Vlastnosti zdroje", "createInternalResourceDialogName": "Jméno", "createInternalResourceDialogSite": "Lokalita", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Neplatný formát IP adresy", "createInternalResourceDialogDestinationPortMin": "Cílový přístav musí být alespoň 1", "createInternalResourceDialogDestinationPortMax": "Cílový přístav musí být nižší než 65536", + "createInternalResourceDialogPortModeRequired": "Protokol, proxy port a cílový port jsou vyžadovány pro režim portu", + "createInternalResourceDialogMode": "Režim", + "createInternalResourceDialogModePort": "Přístav", + "createInternalResourceDialogModeHost": "Hostitel", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Místo určení", + "createInternalResourceDialogDestinationHostDescription": "IP adresa nebo název hostitele zdroje v síti webu.", + "createInternalResourceDialogDestinationCidrDescription": "Rozsah zdrojů CIDR v síti webu.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.", "siteConfiguration": "Konfigurace", "siteAcceptClientConnections": "Přijmout připojení klienta", - "siteAcceptClientConnectionsDescription": "Umožnit ostatním zařízením připojit se prostřednictvím této instance Newt jako brána pomocí klientů.", - "siteAddress": "Adresa webu", - "siteAddressDescription": "Zadejte IP adresu hostitele pro připojení. Toto je interní adresa webu v síti Pangolin pro klienty. Musí spadat do podsítě Org.", + "siteAcceptClientConnectionsDescription": "Povolit uživatelským zařízením a klientům přístup ke zdrojům na tomto webu. To lze později změnit.", + "siteAddress": "Adresa webu (Advanced)", + "siteAddressDescription": "Interní adresa webu. Musí spadat do podsítě organizace.", + "siteNameDescription": "Zobrazovaný název stránky, který lze později změnit.", "autoLoginExternalIdp": "Automatické přihlášení pomocí externího IDP", "autoLoginExternalIdpDescription": "Okamžitě přesměrujte uživatele na externí IDP k ověření.", "selectIdp": "Vybrat IDP", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Od poskytovatele identity nebyla obdržena žádná adresa URL.", "autoLoginErrorGeneratingUrl": "Nepodařilo se vygenerovat ověřovací URL.", "remoteExitNodeManageRemoteExitNodes": "Vzdálené uzly", - "remoteExitNodeDescription": "Hostujte jeden nebo více vlastních vzdálených uzlů, abyste rozšířili síťová připojení a snížili závislost na cloudu", + "remoteExitNodeDescription": "Samohostitelská jedna nebo více vzdálených uzlů pro rozšíření připojení k síti a snížení závislosti na cloudu", "remoteExitNodes": "Uzly", "searchRemoteExitNodes": "Hledat uzly...", "remoteExitNodeAdd": "Přidat uzel", @@ -1627,7 +1686,7 @@ "viewAllButton": "Zobrazit všechny uzly", "strategy": { "title": "Strategie tvorby", - "description": "Vyberte pro manuální konfiguraci vašeho uzlu nebo vygenerujte nové přihlašovací údaje.", + "description": "Zvolte pro ruční nastavení uzlu nebo vygenerování nových pověření.", "adopt": { "title": "Přijmout uzel", "description": "Zvolte tuto možnost, pokud již máte přihlašovací údaje k uzlu." @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Vygenerovaná pověření", - "description": "Použijte tyto generované přihlašovací údaje pro nastavení vašeho uzlu", + "description": "Použijte tyto generované přihlašovací údaje pro nastavení uzlu", "nodeIdTitle": "ID uzlu", "secretTitle": "Tajný klíč", "saveCredentialsTitle": "Přidat přihlašovací údaje do konfigurace", @@ -1725,14 +1784,14 @@ "roleMappingExpressionPlaceholder": "např. obsahuje(skupiny, 'admin') && 'Admin' || 'Member'", "idpGoogleConfiguration": "Konfigurace Google", "idpGoogleConfigurationDescription": "Konfigurace přihlašovacích údajů Google OAuth2", - "idpGoogleClientIdDescription": "Vaše ID klienta Google OAuth2", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", "idpGoogleClientSecretDescription": "Tajný klíč klienta Google OAuth2", "idpAzureConfiguration": "Nastavení Azure Entra ID", - "idpAzureConfigurationDescription": "Nastavte vaše Azure Entra ID OAuth2", + "idpAzureConfigurationDescription": "Konfigurace Azure Entra ID OAuth2", "idpTenantId": "ID tenanta", - "idpTenantIdPlaceholder": "vaše-tenant-id", - "idpAzureTenantIdDescription": "Vaše Azure nájemce ID (nalezeno v přehledu Azure Active Directory – Azure)", - "idpAzureClientIdDescription": "Vaše ID registrace aplikace Azure", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "ID nájemce Azura (nalezeno v přehledu Azure Active Directory - Azure Active Directory - přehled )", + "idpAzureClientIdDescription": "ID klienta pro registraci aplikace Azure", "idpAzureClientSecretDescription": "Tajný klíč registrace aplikace Azure", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Konfigurace Google", "idpAzureConfigurationTitle": "Nastavení Azure Entra ID", "idpTenantIdLabel": "ID tenanta", - "idpAzureClientIdDescription2": "Vaše ID registrace aplikace Azure", + "idpAzureClientIdDescription2": "ID klienta pro registraci aplikace Azure", "idpAzureClientSecretDescription2": "Tajný klíč registrace aplikace Azure", "idpGoogleDescription": "Poskytovatel Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Podsíť", "subnetDescription": "Podsíť pro konfiguraci sítě této organizace.", "authPage": "Auth stránka", - "authPageDescription": "Konfigurace autentizační stránky vaší organizace", + "authPageDescription": "Konfigurace autentizační stránky organizace", "authPageDomain": "Doména ověření stránky", "noDomainSet": "Není nastavena žádná doména", "changeDomain": "Změnit doménu", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Nastavit doménu autentické stránky", "failedToFetchCertificate": "Nepodařilo se načíst certifikát", "failedToRestartCertificate": "Restartování certifikátu se nezdařilo", - "addDomainToEnableCustomAuthPages": "Přidejte doménu pro povolení vlastních ověřovacích stránek pro vaši organizaci", + "addDomainToEnableCustomAuthPages": "Přidat doménu pro povolení vlastních ověřovacích stránek organizace", "selectDomainForOrgAuthPage": "Vyberte doménu pro ověřovací stránku organizace", "domainPickerProvidedDomain": "Poskytnutá doména", "domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" nemohl být platný pro {domain}.", "domainPickerSubdomainSanitized": "Upravená subdoména", "domainPickerSubdomainCorrected": "\"{sub}\" bylo opraveno na \"{sanitized}\"", - "orgAuthSignInTitle": "Přihlaste se do vaší organizace", + "orgAuthSignInTitle": "Přihlásit se k organizaci", "orgAuthChooseIdpDescription": "Chcete-li pokračovat, vyberte svého poskytovatele identity", "orgAuthNoIdpConfigured": "Tato organizace nemá nakonfigurovány žádné poskytovatele identity. Místo toho se můžete přihlásit s vaší Pangolinovou identitou.", "orgAuthSignInWithPangolin": "Přihlásit se pomocí Pangolinu", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Povolit dvoufaktorové ověření", "completeSecuritySteps": "Dokončit bezpečnostní kroky", "securitySettings": "Nastavení zabezpečení", - "securitySettingsDescription": "Konfigurace bezpečnostních zásad pro vaši organizaci", + "securitySettingsDescription": "Konfigurace bezpečnostních pravidel organizace", "requireTwoFactorForAllUsers": "Vyžadovat dvoufaktorové ověření pro všechny uživatele", "requireTwoFactorDescription": "Pokud je povoleno, všichni interní uživatelé v této organizaci musí mít dvoufaktorové ověření povoleno pro přístup k organizaci.", "requireTwoFactorDisabledDescription": "Tato funkce vyžaduje platnou licenci (Enterprise) nebo aktivní předplatné (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Podniková edice", "unlicensed": "Nelicencováno", "beta": "Beta", - "manageClients": "Spravovat klienty", - "manageClientsDescription": "Klienti jsou zařízení, která se mohou připojit k vašim lokalitám", + "manageUserDevices": "Uživatelská zařízení", + "manageUserDevicesDescription": "Zobrazit a spravovat zařízení, která používají uživatelé k soukromím připojení k zdrojům", + "manageMachineClients": "Správa automatických klientů", + "manageMachineClientsDescription": "Vytvořte a spravujte klienty, které servery a systémy používají k soukromím připojování k zdrojům", + "clientsTableUserClients": "Uživatel", + "clientsTableMachineClients": "Stroj", "licenseTableValidUntil": "Platná do", "saasLicenseKeysSettingsTitle": "Podniková licence", "saasLicenseKeysSettingsDescription": "Vygenerovat a spravovat podnikové licence pro instance Pangolin na vlastním hostingu", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "odstranit", "sidebarEnableEnterpriseLicense": "Použít podnikovou licenci", "cannotbeUndone": "To nelze vrátit zpět.", - "toConfirm": "Potvrdit", + "toConfirm": "potvrdit.", "deleteClientQuestion": "Jste si jisti, že chcete odstranit klienta z webu a organizace?", "clientMessageRemove": "Po odstranění se klient již nebude moci připojit k webu.", "sidebarLogs": "Logy", "request": "Žádost", + "requests": "Požadavky", "logs": "Logy", "logsSettingsDescription": "Monitorovat logy shromážděné z této orginizace", "searchLogs": "Hledat logy...", @@ -2020,6 +2084,7 @@ "ip": "IP adresa", "reason": "Důvod", "requestLogs": "Záznamy požadavků", + "requestAnalytics": "Vyžádat analýzu", "host": "Hostitel", "location": "Poloha", "actionLogs": "Záznamy akcí", @@ -2029,6 +2094,7 @@ "logRetention": "Zaznamenávání záznamu", "logRetentionDescription": "Spravovat, jak dlouho jsou různé typy logů uloženy pro tuto organizaci nebo je zakázat", "requestLogsDescription": "Zobrazit podrobné protokoly požadavků pro zdroje v této organizaci", + "requestAnalyticsDescription": "Zobrazit podrobnou analýzu požadavků pro zdroje v této organizaci", "logRetentionRequestLabel": "Zachování logu žádosti", "logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků", "logRetentionAccessLabel": "Zachování záznamu přístupu", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 dní", "logRetention90Days": "90 dní", "logRetentionForever": "Navždy", + "logRetentionEndOfFollowingYear": "Konec následujícího roku", "actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci", "accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci", "licenseRequiredToUse": "Pro použití této funkce je vyžadována licence pro podnikání.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Preferovat Wildcard certifikát", "unverified": "Neověřeno", "domainSetting": "Nastavení domény", - "domainSettingDescription": "Konfigurace nastavení pro vaši doménu", + "domainSettingDescription": "Konfigurace nastavení pro doménu", "preferWildcardCertDescription": "Pokus o vygenerování zástupného certifikátu (vyžaduje správně nakonfigurovaný certifikát).", "recordName": "Název záznamu", "auto": "Automaticky", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Je k dispozici aktualizovaná verze Olm. Pro nejlepší zážitek prosím aktualizujte na nejnovější verzi.", "client": "Zákazník", "proxyProtocol": "Nastavení proxy protokolu", - "proxyProtocolDescription": "Konfigurace Proxy protokolu pro zachování klientských IP adres pro služby TCP/UDP.", + "proxyProtocolDescription": "Konfigurace Proxy protokolu pro zachování klientských IP adres pro služby TCP.", "enableProxyProtocol": "Povolit Proxy protokol", - "proxyProtocolInfo": "Zachovat IP adresy klienta pro TCP/UDP backends", + "proxyProtocolInfo": "Zachovat IP adresu klienta pro TCP zálohy", "proxyProtocolVersion": "Verze proxy protokolu", "version1": " Verze 1 (doporučeno)", "version2": "Verze 2", "versionDescription": "Verze 1 je textová a široce podporovaná. Verze 2 je binární a efektivnější, ale méně kompatibilní.", "warning": "Varování", - "proxyProtocolWarning": "Vaše backend aplikace musí být nakonfigurována, aby mohla být přijata spojení s Proxy protokolem. Pokud vaše backend nepodporuje Proxy protokol, povolením tohoto protokolu dojde k přerušení všech připojení. Ujistěte se, že nastavíte svou backend a důvěřujte hlavičkám Proxy protokolu z Traefik.", + "proxyProtocolWarning": "Aplikace backend musí být nakonfigurována, aby mohla přijímat připojení k Proxy protokolu. Pokud vaše backend nepodporuje Proxy protokol, povolením tohoto protokolu dojde k přerušení všech připojení, takže toto povolíte pouze pokud víte, co děláte. Ujistěte se, že nastavíte svou backend a důvěřujte hlavičkám Proxy protokolu z Traefik.", "restarting": "Restartování...", "manual": "Ruční", "messageSupport": "Podpora zpráv", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Zpráva odeslána!", "supportWillContact": "Brzy budeme v kontaktu!", "selectLogRetention": "Vyberte záznam", + "terms": "Výrazy", + "privacy": "Soukromí", + "security": "Zabezpečení", + "docs": "Dokumenty", + "deviceActivation": "Aktivace zařízení", + "deviceCodeInvalidFormat": "Kód musí být 9 znaků (např. A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Neplatný nebo prošlý kód", + "deviceCodeVerifyFailed": "Ověření kódu zařízení se nezdařilo", + "signedInAs": "Přihlášen jako", + "deviceCodeEnterPrompt": "Zadejte kód zobrazený na zařízení", + "continue": "Pokračovat", + "deviceUnknownLocation": "Neznámá poloha", + "deviceAuthorizationRequested": "Tato autorizace byla vyžádána od {location} na {date}. Ujistěte se, že tomuto zařízení věříte, protože získá přístup k účtu.", + "deviceLabel": "Zařízení: {deviceName}", + "deviceWantsAccess": "chce přistupovat k vašemu účtu", + "deviceExistingAccess": "Stávající přístup:", + "deviceFullAccess": "Úplný přístup k vašemu účtu", + "deviceOrganizationsAccess": "Přístup ke všem organizacím má přístup k vašemu účtu", + "deviceAuthorize": "Autorizovat {applicationName}", + "deviceConnected": "Zařízení připojeno!", + "deviceAuthorizedMessage": "Zařízení má oprávnění k přístupu k vašemu účtu.", + "pangolinCloud": "Pangolin Cloud", + "viewDevices": "Zobrazit zařízení", + "viewDevicesDescription": "Spravovat připojená zařízení", + "noDevices": "Nebyla nalezena žádná zařízení", + "dateCreated": "Datum vytvoření", + "unnamedDevice": "Nepojmenované zařízení", + "deviceQuestionRemove": "Jste si jisti, že chcete odstranit toto zařízení?", + "deviceMessageRemove": "Tuto akci nelze vrátit zpět.", + "deviceDeleteConfirm": "Odstranit zařízení", + "deleteDevice": "Odstranit zařízení", + "errorLoadingDevices": "Chyba při načítání zařízení", + "failedToLoadDevices": "Načtení zařízení se nezdařilo", + "deviceDeleted": "Zařízení odstraněno", + "deviceDeletedDescription": "Zařízení bylo úspěšně smazáno.", + "errorDeletingDevice": "Chyba při odstraňování zařízení", + "failedToDeleteDevice": "Odstranění zařízení se nezdařilo", "showColumns": "Zobrazit sloupce", "hideColumns": "Skrýt sloupce", "columnVisibility": "Viditelnost sloupců", @@ -2111,10 +2215,14 @@ "enableSelected": "Povolit vybrané", "disableSelected": "Zakázat vybrané", "checkSelectedStatus": "Zkontrolovat stav vybraného", + "clients": "Klienti", + "accessClientSelect": "Vyberte klienty stroje", + "resourceClientDescription": "Strojové klienty, kteří mají přístup k tomuto zdroji", + "regenerate": "Regenerovat", "credentials": "Pověření", "savecredentials": "Uložit přihlašovací údaje", - "regeneratecredentials": "Znovu klíče", - "regenerateCredentials": "Regenerovat a uložit vaše přihlašovací údaje", + "regenerateCredentialsButton": "Obnovit přihlašovací údaje", + "regenerateCredentials": "Obnovit přihlašovací údaje", "generatedcredentials": "Vygenerovaná pověření", "copyandsavethesecredentials": "Zkopírovat a ukládat tato pověření", "copyandsavethesecredentialsdescription": "Tyto přihlašovací údaje se znovu nezobrazí po opuštění této stránky. Uložte je bezpečně.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Pověření byla úspěšně obnovena a uložena.", "credentialsSaveError": "Chyba při ukládání pověření", "credentialsSaveErrorDescription": "Došlo k chybě při obnovování a ukládání přihlašovacích údajů.", - "regenerateCredentialsWarning": "Obnovení přihlašovacích údajů zneplatní ty předchozí. Ujistěte se, že aktualizujete všechny konfigurace, které tyto přihlašovací údaje používají.", + "regenerateCredentialsWarning": "Obnovení přihlašovacích údajů zneplatní předchozí a způsobí odpojení. Ujistěte se, že aktualizujete všechny konfigurace, které tyto přihlašovací údaje používají.", "confirm": "Potvrdit", "regenerateCredentialsConfirmation": "Jste si jisti, že chcete obnovit přihlašovací údaje?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Tajný klíč", - "featureDisabledTooltip": "Tato funkce je dostupná pouze v podnikovém plánu a vyžaduje její používání.", "niceId": "Pěkné ID", "niceIdUpdated": "Nice ID aktualizováno", "niceIdUpdatedSuccessfully": "Nice ID úspěšně aktualizováno", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Došlo k chybě při aktualizaci identifikátoru Nice.", "niceIdCannotBeEmpty": "Nice ID nemůže být prázdné", "enterIdentifier": "Zadejte identifikátor", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "Nejste vy? Použijte jiný účet.", + "deviceLoginDeviceRequestingAccessToAccount": "Zařízení žádá o přístup k tomuto účtu.", + "noData": "Žádná data", + "machineClients": "Strojoví klienti", + "install": "Instalovat", + "run": "Spustit", + "clientNameDescription": "Zobrazované jméno klienta, které lze později změnit.", + "clientAddress": "Adresa klienta (Rozšířeno)", + "setupFailedToFetchSubnet": "Nepodařilo se načíst výchozí podsíť", + "setupSubnetAdvanced": "Podsíť (předplacená)", + "setupSubnetDescription": "Podsíť pro vnitřní síť této organizace.", + "siteRegenerateAndDisconnect": "Obnovit a odpojit", + "siteRegenerateAndDisconnectConfirmation": "Opravdu chcete obnovit přihlašovací údaje a odpojit tuto stránku?", + "siteRegenerateAndDisconnectWarning": "Toto obnoví přihlašovací údaje a okamžitě odpojí stránku. Stránka bude muset být restartována s novými přihlašovacími údaji.", + "siteRegenerateCredentialsConfirmation": "Jste si jisti, že chcete obnovit přihlašovací údaje pro tuto stránku?", + "siteRegenerateCredentialsWarning": "Toto obnoví přihlašovací údaje. Stránka zůstane připojena, dokud ji ručně nerestartujete a nepoužijete nové přihlašovací údaje.", + "clientRegenerateAndDisconnect": "Obnovit a odpojit", + "clientRegenerateAndDisconnectConfirmation": "Opravdu chcete obnovit přihlašovací údaje a odpojit tohoto klienta?", + "clientRegenerateAndDisconnectWarning": "Toto obnoví přihlašovací údaje a okamžitě odpojí klienta. Klient bude muset být restartován s novými přihlašovacími údaji.", + "clientRegenerateCredentialsConfirmation": "Opravdu chcete obnovit přihlašovací údaje pro tohoto klienta?", + "clientRegenerateCredentialsWarning": "Toto obnoví přihlašovací údaje. Klient zůstane připojen, dokud jej ručně nerestartujete a nepoužije nové přihlašovací údaje.", + "remoteExitNodeRegenerateAndDisconnect": "Obnovit a odpojit", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Jste si jisti, že chcete obnovit přihlašovací údaje a odpojit tento vzdálený výstupní uzel?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Toto obnoví přihlašovací údaje a okamžitě odpojí vzdálený výstupní uzel. Vzdálený výstupní uzel bude muset být restartován s novými přihlašovacími údaji.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Jste si jisti, že chcete obnovit přihlašovací údaje pro tento vzdálený výstupní uzel?", + "remoteExitNodeRegenerateCredentialsWarning": "Toto obnoví přihlašovací údaje. Vzdálený výstupní uzel zůstane připojen, dokud jej ručně nerestartujete a nepoužijete nové přihlašovací údaje.", + "agent": "Agent" } diff --git a/messages/de-DE.json b/messages/de-DE.json index 29dfab10..333c7052 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1,12 +1,12 @@ { - "setupCreate": "Erstelle eine Organisation, einen Standort und Ressourcen", + "setupCreate": "Organisation, Seite und Ressourcen erstellen", "setupNewOrg": "Neue Organisation", "setupCreateOrg": "Organisation erstellen", "setupCreateResources": "Ressource erstellen", "setupOrgName": "Name der Organisation", - "orgDisplayName": "Anzeigename der Organisation.", + "orgDisplayName": "Dies ist der Anzeigename der Organisation.", "orgId": "Organisations-ID", - "setupIdentifierMessage": "Dies ist eine Eindeutige ID für Ihre Organisation. Diese ist unabhängig vom Anzeigenamen.", + "setupIdentifierMessage": "Dies ist der eindeutige Bezeichner für die Organisation.", "setupErrorIdentifier": "Organisations-ID ist bereits vergeben. Bitte wähle eine andere.", "componentsErrorNoMemberCreate": "Du bist derzeit kein Mitglied einer Organisation. Erstelle eine Organisation, um zu starten.", "componentsErrorNoMember": "Du bist aktuell kein Mitglied einer Organisation.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Sobald die Site entfernt ist, wird sie nicht mehr zugänglich sein. Alle mit der Site verbundenen Ziele werden ebenfalls entfernt.", "siteQuestionRemove": "Sind Sie sicher, dass Sie die Site aus der Organisation entfernen möchten?", "siteManageSites": "Standorte verwalten", - "siteDescription": "Verbindung zum Netzwerk durch sichere Tunnel erlauben", + "siteDescription": "Erstelle und verwalte Sites, um die Verbindung zu privaten Netzwerken zu ermöglichen", "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", + "siteCreateDescription": "Erstellen Sie eine neue Seite, um Ressourcen zu verbinden", "close": "Schließen", "siteErrorCreate": "Fehler beim Erstellen des Standortes", "siteErrorCreateKeyPair": "Schlüsselpaar oder Standardwerte nicht gefunden", @@ -74,7 +74,7 @@ "siteInstallNewt": "Newt installieren", "siteInstallNewtDescription": "Installiere Newt auf deinem System.", "WgConfiguration": "WireGuard Konfiguration", - "WgConfigurationDescription": "Verwende folgende Konfiguration, um dich mit deinem Netzwerk zu verbinden", + "WgConfigurationDescription": "Verwenden Sie folgende Konfiguration, um sich mit dem Netzwerk zu verbinden", "operatingSystem": "Betriebssystem", "commands": "Befehle", "recommended": "Empfohlen", @@ -87,32 +87,32 @@ "siteUpdated": "Standort aktualisiert", "siteUpdatedDescription": "Der Standort wurde aktualisiert.", "siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren", - "siteSettingDescription": "Konfigurieren der Standort Einstellungen", + "siteSettingDescription": "Einstellungen auf der Seite konfigurieren", "siteSetting": "{siteName} Einstellungen", - "siteNewtTunnel": "Newt-Tunnel (empfohlen)", - "siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.", + "siteNewtTunnel": "Neue Seite (empfohlen)", + "siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.", "siteWg": "Einfacher WireGuard Tunnel", "siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.", "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": "Nur lokale Ressourcen. Kein Tunneling. Nur für entfernte Knoten verfügbar.", "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", + "siteTunnelDescription": "Legen Sie fest, wie Sie sich mit der Site verbinden möchten", + "siteNewtCredentials": "Zugangsdaten", + "siteNewtCredentialsDescription": "So wird sich die Seite mit dem Server authentifizieren", + "siteCredentialsSave": "Anmeldedaten speichern", "siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.", "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", + "shareDescription": "Erstelle teilbare Links, um temporären oder permanenten Zugriff auf Proxy-Ressourcen zu gewähren", "shareSearch": "Freigabe-Links suchen...", "shareCreate": "Link erstellen", "shareErrorDelete": "Link konnte nicht gelöscht werden", "shareErrorDeleteMessage": "Fehler beim Löschen des Links", "shareDeleted": "Link gelöscht", "shareDeletedDescription": "Der Link wurde gelöscht", - "shareTokenDescription": "Ihr Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Anfrage-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.", + "shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.", "accessToken": "Zugangs-Token", "usageExamples": "Nutzungsbeispiele", "tokenId": "Token-ID", @@ -121,7 +121,7 @@ "importantNote": "Wichtiger Hinweis", "shareImportantDescription": "Aus Sicherheitsgründen wird die Verwendung von Headern über Abfrageparameter empfohlen, wenn möglich, da Abfrageparameter in Server-Logs oder Browserverlauf protokolliert werden können.", "token": "Token", - "shareTokenSecurety": "Halten Sie Ihr Zugangs-Token sicher. Teilen Sie es nicht in öffentlich zugänglichen Bereichen oder Client-seitigem Code.", + "shareTokenSecurety": "Bewahren Sie das Zugriffstoken sicher. Teilen Sie es nicht in öffentlich zugänglichen Bereichen oder Client-seitigem Code.", "shareErrorFetchResource": "Fehler beim Abrufen der Ressourcen", "shareErrorFetchResourceDescription": "Beim Abrufen der Ressourcen ist ein Fehler aufgetreten", "shareErrorCreate": "Fehler beim Erstellen des Teilen-Links", @@ -144,8 +144,10 @@ "expires": "Gültig bis", "never": "Nie", "shareErrorSelectResource": "Bitte wählen Sie eine Ressource", - "resourceTitle": "Ressourcen verwalten", - "resourceDescription": "Erstellen Sie sichere Proxies für Ihre privaten Anwendungen", + "proxyResourceTitle": "Öffentliche Ressourcen verwalten", + "proxyResourceDescription": "Erstelle und verwalte Ressourcen, die über einen Webbrowser öffentlich zugänglich sind", + "clientResourceTitle": "Private Ressourcen verwalten", + "clientResourceDescription": "Erstelle und verwalte Ressourcen, die nur über einen verbundenen Client zugänglich sind", "resourcesSearch": "Suche Ressourcen...", "resourceAdd": "Ressource hinzufügen", "resourceErrorDelte": "Fehler beim Löschen der Ressource", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Einmal entfernt, wird die Ressource nicht mehr zugänglich sein. Alle mit der Ressource verbundenen Ziele werden ebenfalls entfernt.", "resourceQuestionRemove": "Sind Sie sicher, dass Sie die Ressource aus der Organisation entfernen möchten?", "resourceHTTP": "HTTPS-Ressource", - "resourceHTTPDescription": "Proxy-Anfragen an Ihre App über HTTPS unter Verwendung einer Subdomain oder einer Basis-Domain.", + "resourceHTTPDescription": "Proxy-Anfragen an die App über HTTPS unter Verwendung einer Subdomain oder einer Basis-Domain.", "resourceRaw": "Direkte TCP/UDP Ressource (raw)", - "resourceRawDescription": "Proxy-Anfragen an Ihre App über TCP/UDP mit einer Portnummer.", + "resourceRawDescription": "Proxy-Anfragen an die App über TCP/UDP mit einer Portnummer. Dies funktioniert nur, wenn Sites mit Knoten verbunden sind.", "resourceCreate": "Ressource erstellen", "resourceCreateDescription": "Folgen Sie den Schritten unten, um eine neue Ressource zu erstellen", "resourceSeeAll": "Alle Ressourcen anzeigen", @@ -171,22 +173,22 @@ "noCountryFound": "Kein Land gefunden.", "siteSelectionDescription": "Dieser Standort wird die Verbindung zum Ziel herstellen.", "resourceType": "Ressourcentyp", - "resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten", + "resourceTypeDescription": "Legen Sie fest, wie Sie auf die Ressource zugreifen", "resourceHTTPSSettings": "HTTPS-Einstellungen", - "resourceHTTPSSettingsDescription": "Konfigurieren Sie den Zugriff auf Ihre Ressource über HTTPS", + "resourceHTTPSSettingsDescription": "Konfigurieren Sie den Zugriff auf die Ressource über HTTPS", "domainType": "Domain-Typ", "subdomain": "Subdomain", "baseDomain": "Basis-Domain", - "subdomnainDescription": "Die Subdomain, auf der Ihre Ressource erreichbar sein soll.", + "subdomnainDescription": "Die Subdomäne, auf die die Ressource zugegriffen werden soll.", "resourceRawSettings": "TCP/UDP Einstellungen", - "resourceRawSettingsDescription": "Legen Sie fest, wie auf Ihre Ressource über TCP/UDP zugegriffen wird. Sie ordnen die Ressource einem Port auf dem Pangolin-Server zu, so dass Sie auf die Ressource von server-public-ip:mapped-port zugreifen können.", + "resourceRawSettingsDescription": "Konfigurieren, wie auf die Ressource über TCP/UDP zugegriffen wird", "protocol": "Protokoll", "protocolSelect": "Wählen Sie ein Protokoll", "resourcePortNumber": "Portnummer", "resourcePortNumberDescription": "Die externe Portnummer für Proxy-Anfragen.", "cancel": "Abbrechen", "resourceConfig": "Konfiguration Snippets", - "resourceConfigDescription": "Kopieren und fügen Sie diese Konfigurations-Snippets ein, um Ihre TCP/UDP Ressource einzurichten", + "resourceConfigDescription": "Kopieren und fügen Sie diese Konfigurations-Snippets ein, um die TCP/UDP Ressource einzurichten", "resourceAddEntrypoints": "Traefik: Einstiegspunkte hinzufügen", "resourceExposePorts": "Gerbil: Ports im Docker Compose freigeben", "resourceLearnRaw": "Lernen Sie, wie Sie TCP/UDP Ressourcen konfigurieren", @@ -202,14 +204,14 @@ "proxy": "Proxy", "internal": "Intern", "rules": "Regeln", - "resourceSettingDescription": "Konfigurieren Sie die Einstellungen Ihrer Ressource", + "resourceSettingDescription": "Einstellungen für die Ressource konfigurieren", "resourceSetting": "{resourceName} Einstellungen", - "alwaysAllow": "Immer erlauben", - "alwaysDeny": "Immer ablehnen", + "alwaysAllow": "Auth umgehen", + "alwaysDeny": "Zugriff blockieren", "passToAuth": "Weiterleiten zur Authentifizierung", - "orgSettingsDescription": "Konfiguriere die allgemeinen Einstellungen deiner Organisation", + "orgSettingsDescription": "Organisationseinstellungen konfigurieren", "orgGeneralSettings": "Organisations-Einstellungen", - "orgGeneralSettingsDescription": "Organisationsdetails und Konfiguration verwalten", + "orgGeneralSettingsDescription": "Verwalten Sie die Details und Konfiguration der Organisation", "saveGeneralSettings": "Allgemeine Einstellungen speichern", "saveSettings": "Einstellungen speichern", "orgDangerZone": "Gefahrenzone", @@ -232,7 +234,7 @@ "orgMissing": "Organisations-ID fehlt", "orgMissingMessage": "Einladung kann ohne Organisations-ID nicht neu generiert werden.", "accessUsersManage": "Benutzer verwalten", - "accessUsersDescription": "Lade Benutzer ein und füge sie zu Rollen hinzu, um den Zugriff auf deine Organisation zu verwalten", + "accessUsersDescription": "Benutzer mit Zugriff auf diese Organisation einladen und verwalten", "accessUsersSearch": "Benutzer suchen...", "accessUserCreate": "Benutzer erstellen", "accessUserRemove": "Benutzer entfernen", @@ -241,13 +243,13 @@ "role": "Rolle", "nameRequired": "Name ist erforderlich", "accessRolesManage": "Rollen verwalten", - "accessRolesDescription": "Konfigurieren Sie Rollen, um den Zugriff auf Ihre Organisation zu verwalten", + "accessRolesDescription": "Erstellen und verwalten von Rollen für Benutzer in der Organisation", "accessRolesSearch": "Rollen suchen...", "accessRolesAdd": "Rolle hinzufügen", "accessRoleDelete": "Rolle löschen", "description": "Beschreibung", "inviteTitle": "Einladungen öffnen", - "inviteDescription": "Ihre Einladungen an andere Benutzer verwalten", + "inviteDescription": "Einladungen für andere Benutzer verwalten, der Organisation beizutreten", "inviteSearch": "Einladungen suchen...", "minutes": "Minuten", "hours": "Stunden", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Fehler beim Erstellen des API-Schlüssels", "apiKeysErrorSetPermission": "Fehler beim Setzen der Berechtigungen", "apiKeysCreate": "API-Schlüssel generieren", - "apiKeysCreateDescription": "Generieren Sie einen neuen API-Schlüssel für Ihre Organisation", + "apiKeysCreateDescription": "Einen neuen API-Schlüssel für die Organisation erstellen", "apiKeysGeneralSettings": "Berechtigungen", "apiKeysGeneralSettingsDescription": "Legen Sie fest, was dieser API-Schlüssel tun kann", - "apiKeysList": "Ihr API-Schlüssel", - "apiKeysSave": "Speichern Sie Ihren API-Schlüssel", + "apiKeysList": "Neuer API-Schlüssel", + "apiKeysSave": "API-Schlüssel speichern", "apiKeysSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.", - "apiKeysInfo": "Ihr API-Schlüssel ist:", + "apiKeysInfo": "Der API-Schlüssel ist:", "apiKeysConfirmCopy": "Ich habe den API-Schlüssel kopiert", "generate": "Generieren", "done": "Fertig", @@ -424,7 +426,7 @@ "userCreated": "Benutzer erstellt", "userCreatedDescription": "Der Benutzer wurde erfolgreich erstellt.", "userTypeInternal": "Interner Benutzer", - "userTypeInternalDescription": "Laden Sie einen Benutzer direkt in Ihre Organisation ein.", + "userTypeInternalDescription": "Lade einen Benutzer ein, der Organisation direkt beizutreten.", "userTypeExternal": "Externer Benutzer", "userTypeExternalDescription": "Erstellen Sie einen Benutzer mit einem externen Identitätsanbieter.", "accessUserCreateDescription": "Folgen Sie den Schritten unten, um einen neuen Benutzer zu erstellen", @@ -436,6 +438,16 @@ "inviteEmailSent": "Einladungs-E-Mail an Benutzer senden", "inviteValid": "Gültig für", "selectDuration": "Dauer auswählen", + "selectResource": "Ressource auswählen", + "filterByResource": "Nach Ressource filtern", + "resetFilters": "Filter zurücksetzen", + "totalBlocked": "Anfragen blockiert von Pangolin", + "totalRequests": "Gesamte Anfragen", + "requestsByCountry": "Anfragen nach Land", + "requestsByDay": "Anfragen nach Tag", + "blocked": "Blockiert", + "allowed": "Zulässig", + "topCountries": "Top Länder", "accessRoleSelect": "Rolle auswählen", "inviteEmailSentDescription": "Eine E-Mail mit dem Zugangslink wurde an den Benutzer gesendet. Er muss den Link aufrufen, um die Einladung anzunehmen.", "inviteSentDescription": "Der Benutzer wurde eingeladen. Er muss den unten stehenden Link aufrufen, um die Einladung anzunehmen.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Zugriffskontrollen speichern", "roles": "Rollen", "accessUsersRoles": "Benutzer & Rollen verwalten", - "accessUsersRolesDescription": "Laden Sie Benutzer ein und fügen Sie sie zu Rollen hinzu, um den Zugriff auf Ihre Organisation zu verwalten", + "accessUsersRolesDescription": "Lade Benutzer ein und füge sie zu Rollen hinzu, um den Zugriff auf die Organisation zu verwalten", "key": "Schlüssel", "createdAt": "Erstellt am", "proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domain-Namensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.", "proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domain-Namensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.", "proxyEnableSSL": "SSL aktivieren", - "proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu deinen Zielen.", + "proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu den Zielen.", "target": "Target", "configureTarget": "Ziele konfigurieren", "targetErrorFetch": "Fehler beim Abrufen der Ziele", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Fehler beim Aktualisieren der Ziele", "targetsErrorUpdateDescription": "Beim Aktualisieren der Ziele ist ein Fehler aufgetreten", "targetTlsUpdate": "TLS-Einstellungen aktualisiert", - "targetTlsUpdateDescription": "Ihre TLS-Einstellungen wurden erfolgreich aktualisiert", + "targetTlsUpdateDescription": "TLS Einstellungen wurden erfolgreich aktualisiert", "targetErrorTlsUpdate": "Fehler beim Aktualisieren der TLS-Einstellungen", "targetErrorTlsUpdateDescription": "Beim Aktualisieren der TLS-Einstellungen ist ein Fehler aufgetreten", "proxyUpdated": "Proxy-Einstellungen aktualisiert", - "proxyUpdatedDescription": "Ihre Proxy-Einstellungen wurden erfolgreich aktualisiert", + "proxyUpdatedDescription": "Proxy-Einstellungen wurden erfolgreich aktualisiert", "proxyErrorUpdate": "Fehler beim Aktualisieren der Proxy-Einstellungen", "proxyErrorUpdateDescription": "Beim Aktualisieren der Proxy-Einstellungen ist ein Fehler aufgetreten", - "targetAddr": "IP / Hostname", + "targetAddr": "Host", "targetPort": "Port", "targetProtocol": "Protokoll", "targetTlsSettings": "Sicherheitskonfiguration", - "targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource", + "targetTlsSettingsDescription": "SSL/TLS Einstellungen für die Ressource konfigurieren", "targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen", "targetTlsSni": "TLS Servername", "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 Backend-Diensten zu leiten", + "targetsDescription": "Ziele zur Routenplanung für Backend-Dienste festlegen", "targetStickySessions": "Sticky Sessions aktivieren", "targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.", "methodSelect": "Methode auswählen", "targetSubmit": "Ziel hinzufügen", - "targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an Ihr Backend gesendet werden sollen.", + "targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an das Backend gesendet werden sollen.", "targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.", "targetsSubmit": "Ziele speichern", "addTarget": "Ziel hinzufügen", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Ziel wurde erfolgreich erstellt", "targetErrorCreate": "Fehler beim Erstellen des Ziels", "targetErrorCreateDescription": "Beim Erstellen des Ziels ist ein Fehler aufgetreten", + "tlsServerName": "TLS Servername", + "tlsServerNameDescription": "Der für SNI verwendete TLS-Servername", "save": "Speichern", "proxyAdditional": "Zusätzliche Proxy-Einstellungen", - "proxyAdditionalDescription": "Konfigurieren Sie, wie Ihre Ressource mit Proxy-Einstellungen umgeht", + "proxyAdditionalDescription": "Konfigurieren Sie die Proxy-Einstellungen der Ressource", "proxyCustomHeader": "Benutzerdefinierter Host-Header", "proxyCustomHeaderDescription": "Der Host-Header, der beim Weiterleiten von Anfragen gesetzt werden soll. Leer lassen, um den Standard zu verwenden.", "proxyAdditionalSubmit": "Proxy-Einstellungen speichern", @@ -558,7 +572,7 @@ "rulesMatchType": "Übereinstimmungstyp", "value": "Wert", "rulesAbout": "Über Regeln", - "rulesAboutDescription": "Mit Regeln können Sie den Zugriff auf Ihre Ressource anhand verschiedener Kriterien steuern. Sie können Regeln erstellen, um den Zugriff basierend auf IP-Adresse oder URL-Pfad zu erlauben oder zu verweigern.", + "rulesAboutDescription": "Regeln erlauben es Ihnen, den Zugriff auf die Ressource anhand einer Reihe von Kriterien zu kontrollieren. Sie können Regeln erstellen, die den Zugriff basierend auf IP-Adresse oder URL-Pfad erlauben oder verweigern.", "rulesActions": "Aktionen", "rulesActionAlwaysAllow": "Immer erlauben: Alle Authentifizierungsmethoden umgehen", "rulesActionAlwaysDeny": "Immer verweigern: Alle Anfragen blockieren; keine Authentifizierung möglich", @@ -570,7 +584,7 @@ "rulesEnable": "Regeln aktivieren", "rulesEnableDescription": "Regelauswertung für diese Ressource aktivieren oder deaktivieren", "rulesResource": "Ressourcen-Regelkonfiguration", - "rulesResourceDescription": "Konfigurieren Sie Regeln zur Steuerung des Zugriffs auf Ihre Ressource", + "rulesResourceDescription": "Regeln konfigurieren, um den Zugriff auf die Ressource zu steuern", "ruleSubmit": "Regel hinzufügen", "rulesNoOne": "Keine Regeln. Fügen Sie eine Regel über das Formular hinzu.", "rulesOrder": "Regeln werden nach aufsteigender Priorität ausgewertet.", @@ -586,7 +600,7 @@ "none": "Keine", "unknown": "Unbekannt", "resources": "Ressourcen", - "resourcesDescription": "Ressourcen sind Proxies zu Anwendungen in Ihrem privaten Netzwerk. Erstellen Sie eine Ressource für jeden HTTP/HTTPS- oder direkten TCP/UDP-Dienst in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Site verbunden sein, um private, sichere Konnektivität über einen verschlüsselten WireGuard-Tunnel zu ermöglichen.", + "resourcesDescription": "Ressourcen sind Proxies zu Anwendungen, die im privaten Netzwerk ausgeführt werden. Erstellen Sie eine Ressource für jeden HTTP/HTTPS oder rohen TCP/UDP-Dienst in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private, sichere Verbindung durch einen verschlüsselten WireGuard-Tunnel zu aktivieren.", "resourcesWireGuardConnect": "Sichere Verbindung mit WireGuard-Verschlüsselung", "resourcesMultipleAuthenticationMethods": "Mehrere Authentifizierungsmethoden konfigurieren", "resourcesUsersRolesAccess": "Benutzer- und rollenbasierte Zugriffskontrolle", @@ -597,7 +611,7 @@ "resourceSelect": "Ressource auswählen", "shareLinks": "Freigabe-Links", "share": "Teilbare Links", - "shareDescription2": "Erstellen Sie teilbare Links zu Ihren Ressourcen. Links bieten temporären oder unbegrenzten Zugriff auf Ihre Ressource. Sie können die Ablaufzeit des Links bei der Erstellung konfigurieren.", + "shareDescription2": "Erstellen Sie teilbare Links zu Ressourcen. Links bieten temporären oder unbegrenzten Zugriff auf Ihre Ressource. Sie können die Verfallsdauer des Links beim Erstellen eines Links festlegen.", "shareEasyCreate": "Einfach zu erstellen und zu teilen", "shareConfigurableExpirationDuration": "Konfigurierbare Ablaufzeit", "shareSecureAndRevocable": "Sicher und widerrufbar", @@ -607,19 +621,19 @@ "unknownCommand": "Unbekannter Befehl", "newtErrorFetchReleases": "Fehler beim Abrufen der Release-Informationen: {err}", "newtErrorFetchLatest": "Fehler beim Abrufen der neuesten Version: {err}", - "newtEndpoint": "Newt-Endpunkt", - "newtId": "Newt-ID", - "newtSecretKey": "Newt-Geheimschlüssel", + "newtEndpoint": "Endpunkt", + "newtId": "ID", + "newtSecretKey": "Geheimnis", "architecture": "Architektur", "sites": "Standorte", - "siteWgAnyClients": "Verwenden Sie einen beliebigen WireGuard-Client zur Verbindung. Sie müssen Ihre internen Ressourcen über die Peer-IP adressieren.", + "siteWgAnyClients": "Verwenden Sie jeden WireGuard-Client um sich zu verbinden. Sie müssen interne Ressourcen über die Peer-IP ansprechen.", "siteWgCompatibleAllClients": "Kompatibel mit allen WireGuard-Clients", "siteWgManualConfigurationRequired": "Manuelle Konfiguration erforderlich", "userErrorNotAdminOrOwner": "Benutzer ist kein Administrator oder Eigentümer", "pangolinSettings": "Einstellungen - Pangolin", "accessRoleYour": "Ihre Rolle:", - "accessRoleSelect2": "Wähle eine Rolle", - "accessUserSelect": "Wähle einen Benutzer", + "accessRoleSelect2": "Rollen auswählen", + "accessUserSelect": "Benutzer auswählen", "otpEmailEnter": "E-Mail-Adresse eingeben", "otpEmailEnterDescription": "Drücken Sie Enter, um eine E-Mail nach der Eingabe im Eingabefeld hinzuzufügen.", "otpEmailErrorInvalid": "Ungültige E-Mail-Adresse. Platzhalter (*) muss der gesamte lokale Teil sein.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "PIN-Code festlegen", "resourcePincodeSetupTitleDescription": "Legen Sie einen PIN-Code fest, um diese Ressource zu schützen", "resourceRoleDescription": "Administratoren haben immer Zugriff auf diese Ressource.", - "resourceUsersRoles": "Benutzer & Rollen", + "resourceUsersRoles": "Zugriffskontrolle", "resourceUsersRolesDescription": "Konfigurieren Sie, welche Benutzer und Rollen diese Ressource besuchen können", "resourceUsersRolesSubmit": "Benutzer & Rollen speichern", "resourceWhitelistSave": "Erfolgreich gespeichert", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Ressource übertragen", "siteDestination": "Zielort", "searchSites": "Standorte durchsuchen", + "countries": "Länder", "accessRoleCreate": "Rolle erstellen", "accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.", "accessRoleCreateSubmit": "Rolle erstellen", @@ -766,15 +781,15 @@ "idpOidcConfigure": "OAuth2/OIDC Konfiguration", "idpOidcConfigureDescription": "Konfigurieren Sie die OAuth2/OIDC Anbieter-Endpunkte und Anmeldeinformationen", "idpClientId": "Client-ID", - "idpClientIdDescription": "Die OAuth2 Client-ID von Ihrem Identitätsanbieter", + "idpClientIdDescription": "Die OAuth2-Client-ID des Identity Providers", "idpClientSecret": "Client-Secret", - "idpClientSecretDescription": "Das OAuth2 Client-Secret von Ihrem Identitätsanbieter", + "idpClientSecretDescription": "Das OAuth2-Client-Geheimnis des Identity Providers", "idpAuthUrl": "Autorisierungs-URL", "idpAuthUrlDescription": "Die OAuth2 Autorisierungs-Endpunkt-URL", "idpTokenUrl": "Token-URL", "idpTokenUrlDescription": "Die OAuth2 Token-Endpunkt-URL", "idpOidcConfigureAlert": "Wichtige Information", - "idpOidcConfigureAlertDescription": "Nach dem Erstellen des Identitätsanbieters müssen Sie die Callback-URL in den Einstellungen Ihres Identitätsanbieters konfigurieren. Die Callback-URL wird nach erfolgreicher Erstellung bereitgestellt.", + "idpOidcConfigureAlertDescription": "Nach der Erstellung des Identity Providers müssen Sie die Callback URL in den Einstellungen des Identity Providers konfigurieren. Die Callback-URL wird nach erfolgreicher Erstellung zur Verfügung gestellt.", "idpToken": "Token-Konfiguration", "idpTokenDescription": "Konfigurieren Sie, wie Benutzerinformationen aus dem ID-Token extrahiert werden", "idpJmespathAbout": "Über JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Identitätsanbieter erstellen", "orgPolicies": "Organisationsrichtlinien", "idpSettings": "{idpName} Einstellungen", - "idpCreateSettingsDescription": "Konfigurieren Sie die Einstellungen für Ihren Identitätsanbieter", + "idpCreateSettingsDescription": "Einstellungen für den Identity Provider konfigurieren", "roleMapping": "Rollenzuordnung", "orgMapping": "Organisationszuordnung", "orgPoliciesSearch": "Organisationsrichtlinien suchen...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Identitätsanbieter erfolgreich aktualisiert", "redirectUrl": "Weiterleitungs-URL", "redirectUrlAbout": "Über die Weiterleitungs-URL", - "redirectUrlAboutDescription": "Dies ist die URL, zu der Benutzer nach der Authentifizierung weitergeleitet werden. Sie müssen diese URL in den Einstellungen Ihres Identitätsanbieters konfigurieren.", + "redirectUrlAboutDescription": "Dies ist die URL, zu der Benutzer nach der Authentifizierung umgeleitet werden. Sie müssen diese URL in den Einstellungen des Identity Providers konfigurieren.", "pangolinAuth": "Authentifizierung - Pangolin", "verificationCodeLengthRequirements": "Ihr Verifizierungscode muss 8 Zeichen lang sein.", "errorOccurred": "Ein Fehler ist aufgetreten", @@ -909,6 +924,10 @@ "passwordResetSent": "Wir senden einen Code zum Zurücksetzen des Passworts an diese E-Mail-Adresse.", "passwordResetCode": "Reset-Code", "passwordResetCodeDescription": "Prüfen Sie Ihre E-Mail für den Reset-Code.", + "generatePasswordResetCode": "Passwort zurücksetzen Code generieren", + "passwordResetCodeGenerated": "Passwort zurücksetzen Code generiert", + "passwordResetCodeGeneratedDescription": "Teilen Sie diesen Code mit dem Benutzer. Sie können ihn verwenden, um ihr Passwort zurückzusetzen.", + "passwordResetUrl": "Reset URL", "passwordNew": "Neues Passwort", "passwordNewConfirm": "Neues Passwort bestätigen", "changePassword": "Passwort ändern", @@ -926,6 +945,9 @@ "pincodeAuth": "Authentifizierungscode", "pincodeSubmit2": "Code absenden", "passwordResetSubmit": "Zurücksetzung anfordern", + "passwordResetAlreadyHaveCode": "Passwort zurücksetzen Code eingeben", + "passwordResetSmtpRequired": "Bitte kontaktieren Sie Ihren Administrator", + "passwordResetSmtpRequiredDescription": "Zum Zurücksetzen Ihres Passworts ist ein Passwort erforderlich. Bitte wenden Sie sich an Ihren Administrator.", "passwordBack": "Zurück zum Passwort", "loginBack": "Zurück zur Anmeldung", "signup": "Registrieren", @@ -1080,23 +1102,26 @@ "actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen", "actionListIdpOrgs": "IDP-Organisationen auflisten", "actionUpdateIdpOrg": "IDP-Organisation aktualisieren", - "actionCreateClient": "Kunde erstellen", - "actionDeleteClient": "Kunde löschen", - "actionUpdateClient": "Kunde aktualisieren", + "actionCreateClient": "Endgerät anlegen", + "actionDeleteClient": "Client löschen", + "actionUpdateClient": "Client aktualisieren", "actionListClients": "Clients auflisten", - "actionGetClient": "Kunde holen", + "actionGetClient": "Clients abrufen", "actionCreateSiteResource": "Site-Ressource erstellen", "actionDeleteSiteResource": "Site-Ressource löschen", "actionGetSiteResource": "Site-Ressource abrufen", "actionListSiteResources": "Site-Ressourcen auflisten", "actionUpdateSiteResource": "Site-Ressource aktualisieren", "actionListInvitations": "Einladungen auflisten", + "actionExportLogs": "Logs exportieren", + "actionViewLogs": "Logs anzeigen", "noneSelected": "Keine ausgewählt", "orgNotFound2": "Keine Organisationen gefunden.", "searchProgress": "Suche...", "create": "Erstellen", "orgs": "Organisationen", "loginError": "Beim Anmelden ist ein Fehler aufgetreten", + "loginRequiredForDevice": "Anmeldung ist erforderlich, um Ihr Gerät zu authentifizieren.", "passwordForgot": "Passwort vergessen?", "otpAuth": "Zwei-Faktor-Authentifizierung", "otpAuthDescription": "Geben Sie den Code aus Ihrer Authenticator-App oder einen Ihrer einmaligen Backup-Codes ein.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Zuhause", "sidebarSites": "Standorte", "sidebarResources": "Ressourcen", + "sidebarProxyResources": "Öffentlich", + "sidebarClientResources": "Privat", "sidebarAccessControl": "Zugriffskontrolle", + "sidebarLogsAndAnalytics": "Protokolle & Analysen", "sidebarUsers": "Benutzer", + "sidebarAdmin": "Admin", "sidebarInvitations": "Einladungen", "sidebarRoles": "Rollen", - "sidebarShareableLinks": "Teilbare Links", + "sidebarShareableLinks": "Links", "sidebarApiKeys": "API-Schlüssel", "sidebarSettings": "Einstellungen", "sidebarAllUsers": "Alle Benutzer", "sidebarIdentityProviders": "Identitätsanbieter", "sidebarLicense": "Lizenz", "sidebarClients": "Clients", + "sidebarUserDevices": "Benutzer", + "sidebarMachineClients": "Maschinen", "sidebarDomains": "Domänen", + "sidebarGeneral": "Allgemein", + "sidebarLogAndAnalytics": "Log & Analytik", "sidebarBluePrints": "Baupläne", + "sidebarOrganization": "Organisation", + "sidebarLogsAnalytics": "Analytik", "blueprints": "Baupläne", "blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen", "blueprintAdd": "Blaupause hinzufügen", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Siehe das Ergebnis der angewendeten Blaupause und alle aufgetretenen Fehler", "blueprintInfo": "Blaupauseninformation", "message": "Nachricht", - "blueprintContentsDescription": "Definieren Sie den YAML-Inhalt, der Ihre Infrastruktur beschreibt", + "blueprintContentsDescription": "Den YAML-Inhalt definieren, der die Infrastruktur beschreibt", "blueprintErrorCreateDescription": "Fehler beim Anwenden der Blaupause", "blueprintErrorCreate": "Fehler beim Erstellen der Blaupause", "searchBlueprintProgress": "Blaupausen suchen...", @@ -1230,15 +1265,15 @@ "loading": "Laden", "restart": "Neustart", "domains": "Domänen", - "domainsDescription": "Domains für Ihre Organisation verwalten", + "domainsDescription": "Erstellen und verwalten der in der Organisation verfügbaren Domänen", "domainsSearch": "Domains durchsuchen...", "domainAdd": "Domain hinzufügen", - "domainAddDescription": "Eine neue Domain in Ihrer Organisation registrieren", + "domainAddDescription": "Registrieren Sie eine neue Domäne mit der Organisation", "domainCreate": "Domain erstellen", "domainCreatedDescription": "Domain erfolgreich erstellt", "domainDeletedDescription": "Domain erfolgreich gelöscht", - "domainQuestionRemove": "Sind Sie sicher, dass Sie die Domain von Ihrem Konto entfernen möchten?", - "domainMessageRemove": "Nach dem Entfernen wird die Domain nicht mehr mit Ihrem Konto verknüpft.", + "domainQuestionRemove": "Sind Sie sicher, dass Sie die Domain entfernen möchten?", + "domainMessageRemove": "Nach der Entfernung wird die Domain nicht mehr mit der Organisation verknüpft.", "domainConfirmDelete": "Domain-Löschung bestätigen", "domainDelete": "Domain löschen", "domain": "Domäne", @@ -1257,7 +1292,7 @@ "pending": "Ausstehend", "sidebarBilling": "Abrechnung", "billing": "Abrechnung", - "orgBillingDescription": "Verwalten Sie Ihre Rechnungsinformationen und Abonnements", + "orgBillingDescription": "Zahlungsinformationen und Abonnements verwalten", "github": "GitHub", "pangolinHosted": "Pangolin Hosted", "fossorial": "Fossorial", @@ -1285,7 +1320,7 @@ "productUpdateTitle": "Produkt-Updates", "productUpdateEmpty": "Keine Updates", "dismissAll": "Alle verwerfen", - "pangolinUpdateAvailable": "Neue Version verfügbar", + "pangolinUpdateAvailable": "Update verfügbar", "pangolinUpdateAvailableInfo": "Version {version} ist bereit zur Installation", "pangolinUpdateAvailableReleaseNotes": "Versionshinweise anzeigen", "newtUpdateAvailable": "Update verfügbar", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Verfügbarkeit prüfen...", - "domainPickerNoMatchingDomains": "Keine passenden Domains gefunden. Versuchen Sie es mit einer anderen Domain oder überprüfen Sie die Domain-Einstellungen Ihrer Organisation.", + "domainPickerNoMatchingDomains": "Keine passenden Domains gefunden. Versuchen Sie eine andere Domain oder überprüfen Sie die Domain-Einstellungen der Organisation.", "domainPickerOrganizationDomains": "Organisations-Domains", "domainPickerProvidedDomains": "Bereitgestellte Domains", "domainPickerSubdomain": "Subdomain: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Abonnement ändern", "billingStartSubscription": "Abonnement starten", "billingRecurringCharge": "Wiederkehrende Kosten", - "billingManageSubscriptionSettings": "Verwalten Sie Ihre Abonnement-Einstellungen und Präferenzen", + "billingManageSubscriptionSettings": "Abonnementeinstellungen und -einstellungen verwalten", "billingNoActiveSubscription": "Sie haben kein aktives Abonnement. Starten Sie Ihr Abonnement, um Nutzungslimits zu erhöhen.", "billingFailedToLoadSubscription": "Fehler beim Laden des Abonnements", "billingFailedToLoadUsage": "Fehler beim Laden der Nutzung", @@ -1345,9 +1380,9 @@ "billingPortalError": "Portalfehler", "billingDataUsageInfo": "Wenn Sie mit der Cloud verbunden sind, werden alle Daten über Ihre sicheren Tunnel belastet. Dies schließt eingehenden und ausgehenden Datenverkehr über alle Ihre Websites ein. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Daten werden nicht belastet, wenn Sie Knoten verwenden.", "billingOnlineTimeInfo": "Sie werden belastet, abhängig davon, wie lange Ihre Seiten mit der Cloud verbunden bleiben. Zum Beispiel 44.640 Minuten entspricht einer Site, die 24 Stunden am Tag des Monats läuft. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Die Zeit wird nicht belastet, wenn Sie Knoten verwenden.", - "billingUsersInfo": "Ihnen wird für jeden Benutzer in Ihrer Organisation berechnet. Die Abrechnung erfolgt täglich basierend auf der Anzahl der aktiven Benutzerkonten in Ihrer Organisation.", - "billingDomainInfo": "Ihnen wird jede Domain in Ihrer Organisation berechnet. Die Abrechnung erfolgt täglich, basierend auf der Anzahl der aktiven Domain-Konten in Ihrer Organisation.", - "billingRemoteExitNodesInfo": "Ihnen wird für jeden verwalteten Node in Ihrer Organisation berechnet. Die Abrechnung erfolgt täglich basierend auf der Anzahl der aktiven verwalteten Nodes in Ihrer Organisation.", + "billingUsersInfo": "Sie werden für jeden Benutzer in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven Benutzerkonten in Ihrer Org berechnet.", + "billingDomainInfo": "Sie werden für jede Domain in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven Domain-Konten in Ihrer Org berechnet.", + "billingRemoteExitNodesInfo": "Sie werden für jeden verwalteten Knoten in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven verwalteten Knoten in Ihrer Org berechnet.", "domainNotFound": "Domain nicht gefunden", "domainNotFoundDescription": "Diese Ressource ist deaktiviert, weil die Domain nicht mehr in unserem System existiert. Bitte setzen Sie eine neue Domain für diese Ressource.", "failed": "Fehlgeschlagen", @@ -1430,29 +1465,32 @@ "and": "und", "privacyPolicy": "Datenschutzrichtlinie" }, + "signUpMarketing": { + "keepMeInTheLoop": "Halten Sie mich auf dem Laufenden mit Neuigkeiten, Updates und neuen Funktionen per E-Mail." + }, "siteRequired": "Standort ist erforderlich.", "olmTunnel": "Olm-Tunnel", "olmTunnelDescription": "Nutzen Sie Olm für die Client-Verbindung", "errorCreatingClient": "Fehler beim Erstellen des Clients", "clientDefaultsNotFound": "Standardeinstellungen des Clients nicht gefunden", "createClient": "Client erstellen", - "createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.", + "createClientDescription": "Neuen Client erstellen um auf private Ressourcen zuzugreifen", "seeAllClients": "Alle Clients anzeigen", "clientInformation": "Client-Informationen", "clientNamePlaceholder": "Client-Name", "address": "Adresse", "subnetPlaceholder": "Subnetz", - "addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.", + "addressDescription": "Die interne Adresse des Clients. Muss in das Subnetz der Organisation fallen.", "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", - "clientOlmCredentialsDescription": "So authentifiziert sich Olm beim Server", - "olmEndpoint": "Olm-Endpunkt", - "olmId": "Olm-ID", - "olmSecretKey": "Olm-Geheimschlüssel", - "clientCredentialsSave": "Speichern Sie Ihre Zugangsdaten", + "clientOlmCredentials": "Zugangsdaten", + "clientOlmCredentialsDescription": "So wird sich der Client mit dem Server authentifizieren", + "olmEndpoint": "Endpunkt", + "olmId": "ID", + "olmSecretKey": "Geheimnis", + "clientCredentialsSave": "Anmeldedaten speichern", "clientCredentialsSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.", "generalSettingsDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diesen Client", "clientUpdated": "Client aktualisiert", @@ -1463,9 +1501,7 @@ "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 ü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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Überwachen Sie die Gesundheit dieses Ziels. Bei Bedarf können Sie einen anderen Endpunkt als das Ziel überwachen.", "healthScheme": "Methode", "healthSelectScheme": "Methode auswählen", + "healthCheckPortInvalid": "Der Gesundheitskontroll-Port muss zwischen 1 und 65535 liegen", "healthCheckPath": "Pfad", "healthHostname": "IP / Host", "healthPort": "Port", "healthCheckPathDescription": "Der Pfad zum Überprüfen des Gesundheitszustands.", - "healthyIntervalSeconds": "Gesunder Intervall", - "unhealthyIntervalSeconds": "Ungesunder Intervall", + "healthyIntervalSeconds": "Gesundes Intervall (Sek.)", + "unhealthyIntervalSeconds": "Ungesundes Intervall (Sek)", "IntervalSeconds": "Gesunder Intervall", - "timeoutSeconds": "Timeout", + "timeoutSeconds": "Timeout (Sek.)", "timeIsInSeconds": "Zeit ist in Sekunden", "retryAttempts": "Wiederholungsversuche", "expectedResponseCodes": "Erwartete Antwortcodes", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Domain bearbeiten", "siteName": "Site-Name", "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy-Ressourcen", - "resourcesTableClientResources": "Client-Ressourcen", + "resourcesTableProxyResources": "Öffentlich", + "resourcesTableClientResources": "Privat", "resourcesTableNoProxyResourcesFound": "Keine Proxy-Ressourcen gefunden.", "resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.", "resourcesTableDestination": "Ziel", - "resourcesTableTheseResourcesForUseWith": "Diese Ressourcen sind zur Verwendung mit", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Clients", "resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.", "resourcesTableNoTargets": "Keine Ziele", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Offline", "resourcesTableUnknown": "Unbekannt", "resourcesTableNotMonitored": "Nicht überwacht", - "editInternalResourceDialogEditClientResource": "Client-Ressource bearbeiten", - "editInternalResourceDialogUpdateResourceProperties": "Aktualisieren Sie die Ressourceneigenschaften und die Zielkonfiguration für {resourceName}.", + "editInternalResourceDialogEditClientResource": "Private Ressource bearbeiten", + "editInternalResourceDialogUpdateResourceProperties": "Ressourcen-Konfiguration und Zugriffssteuerung für {resourceName} aktualisieren", "editInternalResourceDialogResourceProperties": "Ressourceneigenschaften", "editInternalResourceDialogName": "Name", "editInternalResourceDialogProtocol": "Protokoll", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat", "editInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein", "editInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein", + "editInternalResourceDialogPortModeRequired": "Protokolle, Proxyport und Zielport werden für den Port-Modus benötigt", + "editInternalResourceDialogMode": "Modus", + "editInternalResourceDialogModePort": "Port", + "editInternalResourceDialogModeHost": "Host", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Ziel", + "editInternalResourceDialogDestinationHostDescription": "Die IP-Adresse oder der Hostname der Ressource im Netzwerk der Website.", + "editInternalResourceDialogDestinationIPDescription": "Die IP-Adresse oder Hostname Adresse der Ressource im Netzwerk der Website.", + "editInternalResourceDialogDestinationCidrDescription": "Der CIDR-Bereich der Ressource im Netzwerk der Website.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Ein optionaler interner DNS-Alias für diese Ressource.", "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.", + "createInternalResourceDialogCreateClientResource": "Private Ressource erstellen", + "createInternalResourceDialogCreateClientResourceDescription": "Erstelle eine neue Ressource, die nur für Clients zugänglich ist, die mit der Organisation verbunden sind", "createInternalResourceDialogResourceProperties": "Ressourceneigenschaften", "createInternalResourceDialogName": "Name", "createInternalResourceDialogSite": "Standort", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat", "createInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein", "createInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein", + "createInternalResourceDialogPortModeRequired": "Protokolle, Proxyport und Zielport werden für den Port-Modus benötigt", + "createInternalResourceDialogMode": "Modus", + "createInternalResourceDialogModePort": "Port", + "createInternalResourceDialogModeHost": "Host", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Ziel", + "createInternalResourceDialogDestinationHostDescription": "Die IP-Adresse oder der Hostname der Ressource im Netzwerk der Website.", + "createInternalResourceDialogDestinationCidrDescription": "Der CIDR-Bereich der Ressource im Netzwerk der Website.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Ein optionaler interner DNS-Alias für diese Ressource.", "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.", + "siteAcceptClientConnectionsDescription": "Erlaube Benutzer-Geräten und Clients Zugriff auf Ressourcen auf dieser Website. Dies kann später geändert werden.", + "siteAddress": "Site-Adresse (Erweitert)", + "siteAddressDescription": "Die interne Adresse der Website. Muss in das Subnetz der Organisation fallen.", + "siteNameDescription": "Der Anzeigename der Site, der später geändert werden kann.", "autoLoginExternalIdp": "Automatische Anmeldung mit externem IDP", "autoLoginExternalIdpDescription": "Leiten Sie den Benutzer sofort zur Authentifizierung an den externen IDP weiter.", "selectIdp": "IDP auswählen", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.", "autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.", "remoteExitNodeManageRemoteExitNodes": "Entfernte Knoten", - "remoteExitNodeDescription": "Self-Hosten Sie einen oder mehrere entfernte Knoten, um Ihr Netzwerk zu erweitern und die Abhängigkeit von der Cloud zu verringern", + "remoteExitNodeDescription": "Self-Hoster einen oder mehrere entfernte Knoten, um die Netzwerkverbindung zu erweitern und die Abhängigkeit von der Cloud zu verringern", "remoteExitNodes": "Knoten", "searchRemoteExitNodes": "Knoten suchen...", "remoteExitNodeAdd": "Knoten hinzufügen", @@ -1623,11 +1682,11 @@ "sidebarRemoteExitNodes": "Entfernte Knoten", "remoteExitNodeCreate": { "title": "Knoten erstellen", - "description": "Erstellen Sie einen neuen Knoten, um Ihr Netzwerk zu erweitern", + "description": "Erstelle einen neuen Knoten, um die Netzwerkverbindung zu erweitern", "viewAllButton": "Alle Knoten anzeigen", "strategy": { "title": "Erstellungsstrategie", - "description": "Wählen Sie diese Option, um Ihren Knoten manuell zu konfigurieren oder neue Zugangsdaten zu generieren.", + "description": "Wählen Sie diese Option, um den Knoten manuell zu konfigurieren oder neue Zugangsdaten zu generieren.", "adopt": { "title": "Node übernehmen", "description": "Wählen Sie dies, wenn Sie bereits die Anmeldedaten für den Knoten haben." @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Generierte Anmeldedaten", - "description": "Verwenden Sie diese generierten Anmeldeinformationen, um Ihren Knoten zu konfigurieren", + "description": "Diese generierten Anmeldeinformationen verwenden, um den Knoten zu konfigurieren", "nodeIdTitle": "Knoten-ID", "secretTitle": "Geheimnis", "saveCredentialsTitle": "Anmeldedaten zur Konfiguration hinzufügen", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Identitätsanbietertyp", "roleMappingExpressionPlaceholder": "z. B. enthalten(Gruppen, 'admin') && 'Admin' || 'Mitglied'", "idpGoogleConfiguration": "Google-Konfiguration", - "idpGoogleConfigurationDescription": "Konfigurieren Sie Ihre Google OAuth2 Zugangsdaten", - "idpGoogleClientIdDescription": "Ihre Google OAuth2 Client-ID", - "idpGoogleClientSecretDescription": "Ihr Google OAuth2 Client Secret", + "idpGoogleConfigurationDescription": "Google OAuth2 Zugangsdaten konfigurieren", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Google OAuth2 Client Geheimnis", "idpAzureConfiguration": "Azure Entra ID Konfiguration", - "idpAzureConfigurationDescription": "Konfigurieren Sie Ihre Azure Entra ID OAuth2 Zugangsdaten", + "idpAzureConfigurationDescription": "Azure Entra ID OAuth2 Zugangsdaten konfigurieren", "idpTenantId": "Mandanten-ID", - "idpTenantIdPlaceholder": "deine Mandant-ID", - "idpAzureTenantIdDescription": "Ihre Azure Tenant-ID (gefunden in Azure Active Directory Übersicht)", - "idpAzureClientIdDescription": "Ihre Azure App Registration Client ID", - "idpAzureClientSecretDescription": "Ihr Azure App Registration Client Secret", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "Azure Tenant ID (gefunden in Azure Active Directory Übersicht)", + "idpAzureClientIdDescription": "Azure App Registration Client ID", + "idpAzureClientSecretDescription": "Azure App Registration Client Geheimnis", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Google-Konfiguration", "idpAzureConfigurationTitle": "Azure Entra ID Konfiguration", "idpTenantIdLabel": "Mandanten-ID", - "idpAzureClientIdDescription2": "Ihre Azure App Registration Client ID", - "idpAzureClientSecretDescription2": "Ihr Azure App Registration Client Secret", + "idpAzureClientIdDescription2": "Azure App Registration Client ID", + "idpAzureClientSecretDescription2": "Azure App Registration Client Geheimnis", "idpGoogleDescription": "Google OAuth2/OIDC Provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Subnetz", "subnetDescription": "Das Subnetz für die Netzwerkkonfiguration dieser Organisation.", "authPage": "Auth Seite", - "authPageDescription": "Konfigurieren Sie die Auth-Seite für Ihre Organisation", + "authPageDescription": "Konfigurieren Sie die Authentifizierungsseite für die Organisation", "authPageDomain": "Domain der Auth Seite", "noDomainSet": "Keine Domain gesetzt", "changeDomain": "Domain ändern", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Domain der Auth Seite festlegen", "failedToFetchCertificate": "Zertifikat konnte nicht abgerufen werden", "failedToRestartCertificate": "Zertifikat konnte nicht neu gestartet werden", - "addDomainToEnableCustomAuthPages": "Fügen Sie eine Domain hinzu, um benutzerdefinierte Authentifizierungsseiten für Ihre Organisation zu aktivieren", + "addDomainToEnableCustomAuthPages": "Füge eine Domain hinzu, um benutzerdefinierte Authentifizierungsseiten für die Organisation zu aktivieren", "selectDomainForOrgAuthPage": "Wählen Sie eine Domain für die Authentifizierungsseite der Organisation", "domainPickerProvidedDomain": "Angegebene Domain", "domainPickerFreeProvidedDomain": "Kostenlose Domain", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" konnte nicht für {domain} gültig gemacht werden.", "domainPickerSubdomainSanitized": "Subdomain bereinigt", "domainPickerSubdomainCorrected": "\"{sub}\" wurde korrigiert zu \"{sanitized}\"", - "orgAuthSignInTitle": "Bei Ihrer Organisation anmelden", + "orgAuthSignInTitle": "In der Organisation anmelden", "orgAuthChooseIdpDescription": "Wähle deinen Identitätsanbieter um fortzufahren", "orgAuthNoIdpConfigured": "Diese Organisation hat keine Identitätsanbieter konfiguriert. Sie können sich stattdessen mit Ihrer Pangolin-Identität anmelden.", "orgAuthSignInWithPangolin": "Mit Pangolin anmelden", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Zwei-Faktor-Authentifizierung aktivieren", "completeSecuritySteps": "Schließe Sicherheitsschritte ab", "securitySettings": "Sicherheitseinstellungen", - "securitySettingsDescription": "Konfigurieren Sie Sicherheitsrichtlinien für Ihre Organisation", + "securitySettingsDescription": "Sicherheitsrichtlinien für die Organisation konfigurieren", "requireTwoFactorForAllUsers": "Zwei-Faktor-Authentifizierung für alle Benutzer erforderlich", "requireTwoFactorDescription": "Wenn aktiviert, müssen alle internen Benutzer in dieser Organisation die Zwei-Faktor-Authentifizierung aktiviert haben, um auf die Organisation zuzugreifen.", "requireTwoFactorDisabledDescription": "Diese Funktion erfordert eine gültige Lizenz (Unternehmen) oder ein aktives Abonnement (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Enterprise Edition", "unlicensed": "Nicht lizenziert", "beta": "Beta", - "manageClients": "Clients verwalten", - "manageClientsDescription": "Clients sind Geräte, die sich mit Ihren Websites verbinden können", + "manageUserDevices": "Benutzer-Geräte", + "manageUserDevicesDescription": "Geräte anschauen und verwalten, die Benutzer für private Verbindungen zu Ressourcen verwenden", + "manageMachineClients": "Maschinen-Clients verwalten", + "manageMachineClientsDescription": "Erstelle und verwalte Clients, die Server und Systeme nutzen, um privat mit Ressourcen zu verbinden", + "clientsTableUserClients": "Benutzer", + "clientsTableMachineClients": "Maschine", "licenseTableValidUntil": "Gültig bis", "saasLicenseKeysSettingsTitle": "Enterprise-Lizenzen", "saasLicenseKeysSettingsDescription": "Erstelle und verwalte Enterprise-Lizenzschlüssel für selbst gehostete Pangolin-Instanzen", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "entfernen", "sidebarEnableEnterpriseLicense": "Enterprise-Lizenz aktivieren", "cannotbeUndone": "Dies kann nicht rückgängig gemacht werden.", - "toConfirm": "bestätigen", + "toConfirm": "bestätigen.", "deleteClientQuestion": "Sind Sie sicher, dass Sie den Client von der Website und der Organisation entfernen möchten?", "clientMessageRemove": "Nach dem Entfernen kann sich der Client nicht mehr mit der Website verbinden.", "sidebarLogs": "Logs", "request": "Anfrage", + "requests": "Anfragen", "logs": "Logs", "logsSettingsDescription": "Aus dieser Orginisierung gesammelte Logs überwachen", "searchLogs": "Logs suchen...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Grund", "requestLogs": "Logs anfordern", + "requestAnalytics": "Analytik anfordern", "host": "Host", "location": "Standort", "actionLogs": "Aktionsprotokolle", @@ -2029,6 +2094,7 @@ "logRetention": "Log-Speicherung", "logRetentionDescription": "Verwalten, wie lange verschiedene Logs für diese Organisation gespeichert werden oder deaktivieren", "requestLogsDescription": "Detaillierte Request-Logs für Ressourcen in dieser Organisation anzeigen", + "requestAnalyticsDescription": "Detaillierte Anfrageanalytik für Ressourcen in dieser Organisation anzeigen", "logRetentionRequestLabel": "Log-Speicherung anfordern", "logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden", "logRetentionAccessLabel": "Zugriffsprotokoll-Speicherung", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 Tage", "logRetention90Days": "90 Tage", "logRetentionForever": "Für immer", + "logRetentionEndOfFollowingYear": "Ende des folgenden Jahres", "actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen", "accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen", "licenseRequiredToUse": "Um diese Funktion nutzen zu können, ist eine Enterprise-Lizenz erforderlich.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Wildcard-Zertifikat bevorzugen", "unverified": "Nicht verifiziert", "domainSetting": "Domänen-Einstellungen", - "domainSettingDescription": "Einstellungen für Ihre Domain konfigurieren", + "domainSettingDescription": "Einstellungen für die Domain konfigurieren", "preferWildcardCertDescription": "Versuch ein Platzhalterzertifikat zu generieren (erfordert einen richtig konfigurierten Zertifikatslöser).", "recordName": "Name des Datensatzes", "auto": "Auto", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Eine aktualisierte Version von Olm ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für die beste Erfahrung.", "client": "Client", "proxyProtocol": "Proxy-Protokoll-Einstellungen", - "proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP/UDP-Dienste zu erhalten.", + "proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP-Dienste zu erhalten.", "enableProxyProtocol": "Proxy-Protokoll aktivieren", - "proxyProtocolInfo": "Client-IP-Adressen für TCP/UDP Backends beibehalten", + "proxyProtocolInfo": "Client-IP-Adressen für TCP-Backends beibehalten", "proxyProtocolVersion": "Proxy-Protokollversion", "version1": " Version 1 (empfohlen)", "version2": "Version 2", "versionDescription": "Die Version 1 ist textbasiert und unterstützt die Version 2, ist binär und effizienter, aber weniger kompatibel.", "warning": "Warnung", - "proxyProtocolWarning": "Ihre Backend-Anwendung muss so konfiguriert sein, dass sie Proxy-Protokoll-Verbindungen akzeptiert. Wenn Ihr Backend das Proxy-Protokoll nicht unterstützt, wird die Aktivierung dieser Option alle Verbindungen zerstören. Stellen Sie sicher, dass Sie Ihr Backend so konfigurieren, dass es Proxy-Protokoll-Header von Traefik vertraut.", + "proxyProtocolWarning": "Die Backend-Anwendung muss so konfiguriert sein, dass Proxy-Protokoll-Verbindungen akzeptiert werden. Wenn Ihr Backend das Proxy-Protokoll nicht unterstützt, wird das Aktivieren aller Verbindungen unterbrochen, so dass Sie dies nur aktivieren, wenn Sie wissen, was Sie tun. Stellen Sie sicher, dass Sie Ihr Backend so konfigurieren, dass es Proxy-Protokoll-Header von Traefik vertraut.", "restarting": "Neustarten...", "manual": "Manuell", "messageSupport": "Nachrichtenunterstützung", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Nachricht gesendet!", "supportWillContact": "Wir werden in Kürze kontaktieren!", "selectLogRetention": "Log-Speicherung auswählen", + "terms": "Begriffe", + "privacy": "Privatsphäre", + "security": "Sicherheit", + "docs": "Texte", + "deviceActivation": "Geräte-Aktivierung", + "deviceCodeInvalidFormat": "Code muss 9 Zeichen lang sein (z.B. A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Ungültiger oder abgelaufener Code", + "deviceCodeVerifyFailed": "Fehler beim Überprüfen des Gerätecodes", + "signedInAs": "Angemeldet als", + "deviceCodeEnterPrompt": "Geben Sie den auf dem Gerät angezeigten Code ein", + "continue": "Weiter", + "deviceUnknownLocation": "Unbekannter Ort", + "deviceAuthorizationRequested": "Diese Autorisierung wurde von {location} auf {date}angefordert. Stellen Sie sicher, dass Sie diesem Gerät vertrauen, da es Zugriff auf das Konto erhält.", + "deviceLabel": "Gerät: {deviceName}", + "deviceWantsAccess": "möchte auf Ihr Konto zugreifen", + "deviceExistingAccess": "Existierender Zugriff:", + "deviceFullAccess": "Voller Zugriff auf Ihr Konto", + "deviceOrganizationsAccess": "Zugriff auf alle Organisationen, auf die Ihr Konto Zugriff hat", + "deviceAuthorize": "{applicationName} autorisieren", + "deviceConnected": "Gerät verbunden!", + "deviceAuthorizedMessage": "Gerät ist berechtigt, auf Ihr Konto zuzugreifen.", + "pangolinCloud": "Pangolin Cloud", + "viewDevices": "Geräte anzeigen", + "viewDevicesDescription": "Verwalten Sie Ihre verbundenen Geräte", + "noDevices": "Keine Geräte gefunden", + "dateCreated": "Erstellungsdatum", + "unnamedDevice": "Unbenanntes Gerät", + "deviceQuestionRemove": "Sind Sie sicher, dass Sie dieses Gerät löschen möchten?", + "deviceMessageRemove": "Diese Aktion kann nicht rückgängig gemacht werden.", + "deviceDeleteConfirm": "Gerät löschen", + "deleteDevice": "Gerät löschen", + "errorLoadingDevices": "Fehler beim Laden der Geräte", + "failedToLoadDevices": "Fehler beim Laden der Geräte", + "deviceDeleted": "Gerät gelöscht", + "deviceDeletedDescription": "Das Gerät wurde erfolgreich gelöscht.", + "errorDeletingDevice": "Fehler beim Löschen des Geräts", + "failedToDeleteDevice": "Gerät konnte nicht gelöscht werden", "showColumns": "Spalten anzeigen", "hideColumns": "Spalten ausblenden", "columnVisibility": "Spaltensichtbarkeit", @@ -2110,10 +2214,15 @@ "selectedResources": "Ausgewählte Ressourcen", "enableSelected": "Ausgewählte aktivieren", "disableSelected": "Ausgewählte deaktivieren", + "checkSelectedStatus": "Status der Auswahl überprüfen", + "clients": "Clients", + "accessClientSelect": "Maschinen-Clients auswählen", + "resourceClientDescription": "Maschinelle Clients die auf diese Ressource zugreifen können", + "regenerate": "Neu generieren", "credentials": "Zugangsdaten", "savecredentials": "Zugangsdaten speichern", - "regeneratecredentials": "Re-Key", - "regenerateCredentials": "Regenerieren und speichern Sie Ihre Zugangsdaten", + "regenerateCredentialsButton": "Zugangsdaten neu generieren", + "regenerateCredentials": "Zugangsdaten neu generieren", "generatedcredentials": "Generierte Zugangsdaten", "copyandsavethesecredentials": "Diese Zugangsdaten kopieren und speichern", "copyandsavethesecredentialsdescription": "Diese Zugangsdaten werden nach dem Verlassen dieser Seite nicht mehr angezeigt. Speichern Sie sie jetzt sicher.", @@ -2121,13 +2230,12 @@ "credentialsSavedDescription": "Zugangsdaten wurden neu erstellt und erfolgreich gespeichert.", "credentialsSaveError": "Fehler beim Speichern der Zugangsdaten", "credentialsSaveErrorDescription": "Beim Erneuern und Speichern der Zugangsdaten ist ein Fehler aufgetreten.", - "regenerateCredentialsWarning": "Das Regenerieren von Zugangsdaten wird die vorhergehenden ungültig machen. Bitte aktualisieren die Konfigurationen, welche diese Zugangsdaten verwenden.", + "regenerateCredentialsWarning": "Das erneute Erzeugen von Anmeldedaten wird die vorhergehenden ungültig machen und eine Trennung der Verbindung verursachen. Stellen Sie sicher, dass Sie Konfigurationen aktualisieren, die diese Zugangsdaten verwenden.", "confirm": "Bestätigen", "regenerateCredentialsConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten neu generieren möchten?", "endpoint": "Endpunkt", "Id": "ID", "SecretKey": "Geheimer Schlüssel", - "featureDisabledTooltip": "Diese Funktion ist nur im Enterprise-Plan verfügbar und erfordert eine Lizenz, um sie zu nutzen.", "niceId": "Schöne ID", "niceIdUpdated": "Schöne ID aktualisiert", "niceIdUpdatedSuccessfully": "Nice ID erfolgreich aktualisiert", @@ -2136,5 +2244,31 @@ "niceIdCannotBeEmpty": "Nizza-ID darf nicht leer sein", "enterIdentifier": "Identifikator eingeben", "identifier": "Identifier", - "checkSelectedStatus": "Status der Auswahl überprüfen" + "deviceLoginUseDifferentAccount": "Nicht du? Verwenden Sie ein anderes Konto.", + "deviceLoginDeviceRequestingAccessToAccount": "Ein Gerät fordert Zugriff auf dieses Konto an.", + "noData": "Keine Daten", + "machineClients": "Maschinen-Clients", + "install": "Installieren", + "run": "Ausführen", + "clientNameDescription": "Der Anzeigename des Clients, der später geändert werden kann.", + "clientAddress": "Clientadresse (Erweitert)", + "setupFailedToFetchSubnet": "Fehler beim Abrufen des Standard-Subnetzes", + "setupSubnetAdvanced": "Subnetz (Fortgeschritten)", + "setupSubnetDescription": "Das Subnetz für das interne Netzwerk dieser Organisation.", + "siteRegenerateAndDisconnect": "Regenerieren und trennen", + "siteRegenerateAndDisconnectConfirmation": "Sind Sie sicher, dass Sie die Anmeldedaten neu generieren und diese Website trennen möchten?", + "siteRegenerateAndDisconnectWarning": "Dies wird die Anmeldeinformationen neu generieren und die Website sofort trennen. Die Seite muss mit den neuen Anmeldeinformationen neu gestartet werden.", + "siteRegenerateCredentialsConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten für diese Seite neu generieren möchten?", + "siteRegenerateCredentialsWarning": "Dies wird die Anmeldeinformationen neu generieren. Die Seite bleibt verbunden, bis Sie sie manuell neu starten und die neuen Anmeldeinformationen verwenden.", + "clientRegenerateAndDisconnect": "Regenerieren und trennen", + "clientRegenerateAndDisconnectConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten neu generieren und diesen Client trennen möchten?", + "clientRegenerateAndDisconnectWarning": "Dies wird die Anmeldeinformationen neu generieren und den Client sofort trennen. Der Client muss mit den neuen Anmeldeinformationen neu gestartet werden.", + "clientRegenerateCredentialsConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten für diesen Client neu generieren möchten?", + "clientRegenerateCredentialsWarning": "Dies wird die Anmeldeinformationen neu generieren. Der Client bleibt verbunden, bis Sie ihn manuell neu starten und die neuen Anmeldeinformationen verwenden.", + "remoteExitNodeRegenerateAndDisconnect": "Regenerieren und trennen", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten neu generieren und diesen Remote-Exit-Knoten trennen möchten?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Dies wird die Anmeldeinformationen neu generieren und den Remote-Exit-Knoten sofort trennen. Der Remote-Exit-Knoten muss mit den neuen Anmeldeinformationen neu gestartet werden.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten für diesen Remote-Exit-Knoten neu generieren möchten?", + "remoteExitNodeRegenerateCredentialsWarning": "Dies wird die Anmeldeinformationen neu generieren. Der Remote-Exit-Knoten bleibt verbunden, bis Sie ihn manuell neu starten und die neuen Anmeldeinformationen verwenden.", + "agent": "Agent" } diff --git a/messages/en-US.json b/messages/en-US.json index 0d5e80ea..5ae94f7e 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -144,9 +144,9 @@ "expires": "Expires", "never": "Never", "shareErrorSelectResource": "Please select a resource", - "proxyResourceTitle": "Manage Proxy Resources", + "proxyResourceTitle": "Manage Public Resources", "proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser", - "clientResourceTitle": "Manage Client Resources", + "clientResourceTitle": "Manage Private Resources", "clientResourceDescription": "Create and manage resources that are only accessible through a connected client", "resourcesSearch": "Search resources...", "resourceAdd": "Add Resource", @@ -181,7 +181,7 @@ "baseDomain": "Base Domain", "subdomnainDescription": "The subdomain where the resource will be accessible.", "resourceRawSettings": "TCP/UDP Settings", - "resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP. You map the resource to a port on the host Pangolin server, so you can access the resource from server-public-ip:mapped-port.", + "resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP", "protocol": "Protocol", "protocolSelect": "Select a protocol", "resourcePortNumber": "Port Number", @@ -499,7 +499,7 @@ "proxyUpdatedDescription": "Proxy settings have been updated successfully", "proxyErrorUpdate": "Failed to update proxy settings", "proxyErrorUpdateDescription": "An error occurred while updating proxy settings", - "targetAddr": "IP / Hostname", + "targetAddr": "Host", "targetPort": "Port", "targetProtocol": "Protocol", "targetTlsSettings": "Secure Connection Configuration", @@ -1176,8 +1176,8 @@ "sidebarHome": "Home", "sidebarSites": "Sites", "sidebarResources": "Resources", - "sidebarProxyResources": "Proxy Resources", - "sidebarClientResources": "Client Resources", + "sidebarProxyResources": "Public", + "sidebarClientResources": "Private", "sidebarAccessControl": "Access Control", "sidebarLogsAndAnalytics": "Logs & Analytics", "sidebarUsers": "Users", @@ -1191,14 +1191,14 @@ "sidebarIdentityProviders": "Identity Providers", "sidebarLicense": "License", "sidebarClients": "Clients", - "sidebarUserDevices": "User Devices", - "sidebarMachineClients": "Machine Clients", + "sidebarUserDevices": "Users", + "sidebarMachineClients": "Machines", "sidebarDomains": "Domains", "sidebarGeneral": "General", "sidebarLogAndAnalytics": "Log & Analytics", "sidebarBluePrints": "Blueprints", "sidebarOrganization": "Organization", - "sidebarLogsAnalytics": "Request Analytics", + "sidebarLogsAnalytics": "Analytics", "blueprints": "Blueprints", "blueprintsDescription": "Apply declarative configurations and view previous runs", "blueprintAdd": "Add Blueprint", @@ -1323,9 +1323,9 @@ "productUpdateTitle": "Product Updates", "productUpdateEmpty": "No updates", "dismissAll": "Dismiss all", - "pangolinUpdateAvailable": "New version available", + "pangolinUpdateAvailable": "Update Available", "pangolinUpdateAvailableInfo": "Version {version} is ready to install", - "pangolinUpdateAvailableReleaseNotes": "View release notes", + "pangolinUpdateAvailableReleaseNotes": "View Release Notes", "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", "domainPickerEnterDomain": "Domain", @@ -1580,7 +1580,7 @@ "resourcesTableOffline": "Offline", "resourcesTableUnknown": "Unknown", "resourcesTableNotMonitored": "Not monitored", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogEditClientResource": "Edit Private Resource", "editInternalResourceDialogUpdateResourceProperties": "Update the resource configuration and access controls for {resourceName}", "editInternalResourceDialogResourceProperties": "Resource Properties", "editInternalResourceDialogName": "Name", @@ -1614,7 +1614,7 @@ "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", + "createInternalResourceDialogCreateClientResource": "Create Private Resource", "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization", "createInternalResourceDialogResourceProperties": "Resource Properties", "createInternalResourceDialogName": "Name", @@ -2056,7 +2056,7 @@ "pathRewriteStripLabel": "strip", "sidebarEnableEnterpriseLicense": "Enable Enterprise License", "cannotbeUndone": "This can not be undone.", - "toConfirm": "to confirm", + "toConfirm": "to confirm.", "deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?", "clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.", "sidebarLogs": "Logs", @@ -2111,6 +2111,7 @@ "logRetention30Days": "30 days", "logRetention90Days": "90 days", "logRetentionForever": "Forever", + "logRetentionEndOfFollowingYear": "End of following year", "actionLogsDescription": "View a history of actions performed in this organization", "accessLogsDescription": "View access auth requests for resources in this organization", "licenseRequiredToUse": "An Enterprise license is required to use this feature.", @@ -2223,8 +2224,8 @@ "regenerate": "Regenerate", "credentials": "Credentials", "savecredentials": "Save Credentials", - "regeneratecredentials": "Re-key", - "regenerateCredentials": "Regenerate and save your credentials", + "regenerateCredentialsButton": "Regenerate Credentials", + "regenerateCredentials": "Regenerate Credentials", "generatedcredentials": "Generated Credentials", "copyandsavethesecredentials": "Copy and save these credentials", "copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.", @@ -2232,13 +2233,12 @@ "credentialsSavedDescription": "Credentials have been regenerated and saved successfully.", "credentialsSaveError": "Credentials Save Error", "credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.", - "regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.", + "regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones and cause a disconnection. Make sure to update any configurations that use these credentials.", "confirm": "Confirm", "regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Secret Key", - "featureDisabledTooltip": "This feature is only available in the enterprise plan and require a license to use it.", "niceId": "Nice ID", "niceIdUpdated": "Nice ID Updated", "niceIdUpdatedSuccessfully": "Nice ID Updated Successfully", @@ -2257,5 +2257,21 @@ "clientAddress": "Client Address (Advanced)", "setupFailedToFetchSubnet": "Failed to fetch default subnet", "setupSubnetAdvanced": "Subnet (Advanced)", - "setupSubnetDescription": "The subnet for this organization's internal network." + "setupSubnetDescription": "The subnet for this organization's internal network.", + "siteRegenerateAndDisconnect": "Regenerate and Disconnect", + "siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?", + "siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.", + "siteRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this site?", + "siteRegenerateCredentialsWarning": "This will regenerate the credentials. The site will stay connected until you manually restart it and use the new credentials.", + "clientRegenerateAndDisconnect": "Regenerate and Disconnect", + "clientRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this client?", + "clientRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the client. The client will need to be restarted with the new credentials.", + "clientRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this client?", + "clientRegenerateCredentialsWarning": "This will regenerate the credentials. The client will stay connected until you manually restart it and use the new credentials.", + "remoteExitNodeRegenerateAndDisconnect": "Regenerate and Disconnect", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this remote exit node?", + "remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?", + "remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.", + "agent": "Agent" } diff --git a/messages/es-ES.json b/messages/es-ES.json index 1b33c928..1ff02bca 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -1,12 +1,12 @@ { - "setupCreate": "Crea tu organización, sitio y recursos", + "setupCreate": "Crear la organización, el sitio y los recursos", "setupNewOrg": "Nueva organización", "setupCreateOrg": "Crear organización", "setupCreateResources": "Crear Recursos", "setupOrgName": "Nombre de la organización", - "orgDisplayName": "Este es el nombre mostrado de su organización.", + "orgDisplayName": "Este es el nombre mostrado de la organización.", "orgId": "ID de la organización", - "setupIdentifierMessage": "Este es el identificador único para su organización. Esto es independiente del nombre de la pantalla.", + "setupIdentifierMessage": "Este es el identificador único para la organización.", "setupErrorIdentifier": "El ID de la organización ya está en uso. Por favor, elija uno diferente.", "componentsErrorNoMemberCreate": "Actualmente no eres miembro de ninguna organización. Crea una organización para empezar.", "componentsErrorNoMember": "Actualmente no eres miembro de ninguna organización.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Una vez eliminado, el sitio ya no será accesible. Todos los objetivos asociados con el sitio también serán eliminados.", "siteQuestionRemove": "¿Está seguro que desea eliminar el sitio de la organización?", "siteManageSites": "Administrar Sitios", - "siteDescription": "Permitir conectividad a tu red a través de túneles seguros", + "siteDescription": "Crear y administrar sitios para permitir la conectividad a redes privadas", "siteCreate": "Crear sitio", "siteCreateDescription2": "Siga los pasos siguientes para crear y conectar un nuevo sitio", - "siteCreateDescription": "Crear un nuevo sitio para comenzar a conectar sus recursos", + "siteCreateDescription": "Crear un nuevo sitio para empezar a conectar recursos", "close": "Cerrar", "siteErrorCreate": "Error al crear el sitio", "siteErrorCreateKeyPair": "Por defecto no se encuentra el par de claves o el sitio", @@ -74,7 +74,7 @@ "siteInstallNewt": "Instalar Newt", "siteInstallNewtDescription": "Recibe Newt corriendo en tu sistema", "WgConfiguration": "Configuración de Wirex Guard", - "WgConfigurationDescription": "Utilice la siguiente configuración para conectarse a su red", + "WgConfigurationDescription": "Utilice la siguiente configuración para conectarse a la red", "operatingSystem": "Sistema operativo", "commands": "Comandos", "recommended": "Recomendado", @@ -87,32 +87,32 @@ "siteUpdated": "Sitio actualizado", "siteUpdatedDescription": "El sitio ha sido actualizado.", "siteGeneralDescription": "Configurar la configuración general de este sitio", - "siteSettingDescription": "Configurar la configuración de su sitio", + "siteSettingDescription": "Configurar los ajustes en el sitio", "siteSetting": "Ajustes {siteName}", - "siteNewtTunnel": "Túnel Nuevo (Recomendado)", - "siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en tu red. Sin configuración adicional.", + "siteNewtTunnel": "Sitio nuevo (recomendado)", + "siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en cualquier red. Sin configuración extra.", "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": "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": "Solo recursos locales. No hay túneles. Sólo disponible en nodos remotos.", "siteSeeAll": "Ver todos los sitios", - "siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio", - "siteNewtCredentials": "Credenciales nuevas", - "siteNewtCredentialsDescription": "Así es como Newt se autentificará con el servidor", - "siteCredentialsSave": "Guarda tus credenciales", + "siteTunnelDescription": "Determina cómo quieres conectarte al sitio", + "siteNewtCredentials": "Credenciales", + "siteNewtCredentialsDescription": "Así es como el sitio se autentificará con el servidor", + "siteCredentialsSave": "Guardar las credenciales", "siteCredentialsSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.", "siteInfo": "Información del sitio", "status": "Estado", "shareTitle": "Administrar Enlaces de Compartir", - "shareDescription": "Crear enlaces compartidos para conceder acceso temporal o permanente a tus recursos", + "shareDescription": "Crear enlaces compartidos para conceder acceso temporal o permanente a recursos proxy", "shareSearch": "Buscar enlaces compartidos...", "shareCreate": "Crear enlace Compartir", "shareErrorDelete": "Error al eliminar el enlace", "shareErrorDeleteMessage": "Se ha producido un error al eliminar el enlace", "shareDeleted": "Enlace eliminado", "shareDeletedDescription": "El enlace ha sido eliminado", - "shareTokenDescription": "Su token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.", + "shareTokenDescription": "El token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.", "accessToken": "Token de acceso", "usageExamples": "Ejemplos de uso", "tokenId": "ID de token", @@ -121,7 +121,7 @@ "importantNote": "Nota Importante", "shareImportantDescription": "Por razones de seguridad, el uso de cabeceras se recomienda sobre parámetros de consulta cuando sea posible, ya que los parámetros de consulta pueden ser registrados en los registros del servidor o en el historial del navegador.", "token": "Token", - "shareTokenSecurety": "Mantenga su token de acceso seguro. No lo comparta en áreas de acceso público o código del lado del cliente.", + "shareTokenSecurety": "Mantenga seguro el token de acceso. No lo comparta en áreas de acceso público o código del lado del cliente.", "shareErrorFetchResource": "No se pudo obtener recursos", "shareErrorFetchResourceDescription": "Se ha producido un error al recuperar los recursos", "shareErrorCreate": "Error al crear el enlace compartir", @@ -144,8 +144,10 @@ "expires": "Caduca", "never": "Nunca", "shareErrorSelectResource": "Por favor, seleccione un recurso", - "resourceTitle": "Administrar recursos", - "resourceDescription": "Crea proxies seguros para tus aplicaciones privadas", + "proxyResourceTitle": "Administrar recursos públicos", + "proxyResourceDescription": "Crear y administrar recursos que sean accesibles públicamente a través de un navegador web", + "clientResourceTitle": "Administrar recursos privados", + "clientResourceDescription": "Crear y administrar recursos que sólo son accesibles a través de un cliente conectado", "resourcesSearch": "Buscar recursos...", "resourceAdd": "Añadir Recurso", "resourceErrorDelte": "Error al eliminar el recurso", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Una vez eliminado, el recurso ya no será accesible. Todos los objetivos asociados con el recurso también serán eliminados.", "resourceQuestionRemove": "¿Está seguro que desea eliminar el recurso de la organización?", "resourceHTTP": "HTTPS Recurso", - "resourceHTTPDescription": "Solicitudes de proxy a tu aplicación sobre HTTPS usando un subdominio o dominio base.", + "resourceHTTPDescription": "Solicitudes de proxy a la aplicación sobre HTTPS usando un subdominio o dominio base.", "resourceRaw": "Recurso TCP/UDP sin procesar", - "resourceRawDescription": "Solicitudes de proxy a tu aplicación a través de TCP/UDP usando un número de puerto.", + "resourceRawDescription": "Solicitudes de proxy a la aplicación a través de TCP/UDP usando un número de puerto. Esto solo funciona cuando los sitios están conectados a nodos.", "resourceCreate": "Crear Recurso", "resourceCreateDescription": "Siga los siguientes pasos para crear un nuevo recurso", "resourceSeeAll": "Ver todos los recursos", @@ -171,22 +173,22 @@ "noCountryFound": "Ningún país encontrado.", "siteSelectionDescription": "Este sitio proporcionará conectividad al objetivo.", "resourceType": "Tipo de recurso", - "resourceTypeDescription": "Determina cómo quieres acceder a tu recurso", + "resourceTypeDescription": "Determina cómo acceder al recurso", "resourceHTTPSSettings": "Configuración HTTPS", - "resourceHTTPSSettingsDescription": "Configurar cómo se accederá a tu recurso a través de HTTPS", + "resourceHTTPSSettingsDescription": "Configurar cómo se accederá al recurso a través de HTTPS", "domainType": "Tipo de dominio", "subdomain": "Subdominio", "baseDomain": "Dominio base", - "subdomnainDescription": "El subdominio al que su recurso será accesible.", + "subdomnainDescription": "El subdominio al que el recurso será accesible.", "resourceRawSettings": "Configuración TCP/UDP", - "resourceRawSettingsDescription": "Configure cómo se accederá a su recurso a través de TCP/UDP. Mapeas el recurso a un puerto en el servidor Pangolin host, así puedes acceder al recurso desde el servidor-public-ip:mapped-port.", + "resourceRawSettingsDescription": "Configurar cómo se accederá al recurso a través de TCP/UDP", "protocol": "Protocolo", "protocolSelect": "Seleccionar un protocolo", "resourcePortNumber": "Número de puerto", "resourcePortNumberDescription": "El número de puerto externo a las solicitudes de proxy.", "cancel": "Cancelar", "resourceConfig": "Fragmentos de configuración", - "resourceConfigDescription": "Copia y pega estos fragmentos de configuración para configurar tu recurso TCP/UDP", + "resourceConfigDescription": "Copia y pega estos fragmentos de configuración para configurar el recurso TCP/UDP", "resourceAddEntrypoints": "Traefik: Añadir puntos de entrada", "resourceExposePorts": "Gerbil: Exponer puertos en Docker Compose", "resourceLearnRaw": "Aprende cómo configurar los recursos TCP/UDP", @@ -202,14 +204,14 @@ "proxy": "Proxy", "internal": "Interno", "rules": "Reglas", - "resourceSettingDescription": "Configure la configuración de su recurso", + "resourceSettingDescription": "Configurar la configuración del recurso", "resourceSetting": "Ajustes {resourceName}", - "alwaysAllow": "Permitir siempre", - "alwaysDeny": "Denegar siempre", + "alwaysAllow": "Autorización Bypass", + "alwaysDeny": "Bloquear acceso", "passToAuth": "Pasar a Autenticación", - "orgSettingsDescription": "Configurar la configuración general de su organización", + "orgSettingsDescription": "Configurar la configuración de la organización", "orgGeneralSettings": "Configuración de la organización", - "orgGeneralSettingsDescription": "Administra los detalles y la configuración de tu organización", + "orgGeneralSettingsDescription": "Administrar los detalles y la configuración de la organización", "saveGeneralSettings": "Guardar ajustes generales", "saveSettings": "Guardar ajustes", "orgDangerZone": "Zona de peligro", @@ -232,7 +234,7 @@ "orgMissing": "Falta el ID de la organización", "orgMissingMessage": "No se puede regenerar la invitación sin el ID de la organización.", "accessUsersManage": "Administrar usuarios", - "accessUsersDescription": "Invitar usuarios y añadirlos a roles para administrar el acceso a su organización", + "accessUsersDescription": "Invitar y administrar usuarios con acceso a esta organización", "accessUsersSearch": "Buscar usuarios...", "accessUserCreate": "Crear usuario", "accessUserRemove": "Eliminar usuario", @@ -241,13 +243,13 @@ "role": "Rol", "nameRequired": "Se requiere nombre", "accessRolesManage": "Administrar roles", - "accessRolesDescription": "Configurar roles para administrar el acceso a su organización", + "accessRolesDescription": "Crear y administrar roles para usuarios en la organización", "accessRolesSearch": "Buscar roles...", "accessRolesAdd": "Añadir rol", "accessRoleDelete": "Eliminar rol", "description": "Descripción", "inviteTitle": "Invitaciones abiertas", - "inviteDescription": "Administra tus invitaciones a otros usuarios", + "inviteDescription": "Administrar invitaciones para que otros usuarios se unan a la organización", "inviteSearch": "Buscar invitaciones...", "minutes": "Minutos", "hours": "Horas", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Error al crear la clave API", "apiKeysErrorSetPermission": "Error al establecer permisos", "apiKeysCreate": "Generar clave API", - "apiKeysCreateDescription": "Generar una nueva clave API para su organización", + "apiKeysCreateDescription": "Generar una nueva clave API para la organización", "apiKeysGeneralSettings": "Permisos", "apiKeysGeneralSettingsDescription": "Determinar qué puede hacer esta clave API", - "apiKeysList": "Tu clave API", - "apiKeysSave": "Guarda tu clave API", + "apiKeysList": "Nueva Clave API", + "apiKeysSave": "Guardar la clave API", "apiKeysSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.", - "apiKeysInfo": "Tu clave API es:", + "apiKeysInfo": "La clave API es:", "apiKeysConfirmCopy": "He copiado la clave API", "generate": "Generar", "done": "Hecho", @@ -424,7 +426,7 @@ "userCreated": "Usuario creado", "userCreatedDescription": "El usuario se ha creado correctamente.", "userTypeInternal": "Usuario interno", - "userTypeInternalDescription": "Invita a un usuario a unirse a tu organización directamente.", + "userTypeInternalDescription": "Invitar a un usuario a unirse a la organización directamente.", "userTypeExternal": "Usuario externo", "userTypeExternalDescription": "Crear un usuario con un proveedor de identidad externo.", "accessUserCreateDescription": "Siga los pasos siguientes para crear un nuevo usuario", @@ -436,6 +438,16 @@ "inviteEmailSent": "Enviar correo de invitación al usuario", "inviteValid": "Válido para", "selectDuration": "Seleccionar duración", + "selectResource": "Seleccionar Recurso", + "filterByResource": "Filtrar por Recurso", + "resetFilters": "Reiniciar filtros", + "totalBlocked": "Solicitudes bloqueadas por Pangolin", + "totalRequests": "Solicitudes totales", + "requestsByCountry": "Solicitudes por país", + "requestsByDay": "Solicitudes por día", + "blocked": "Bloqueado", + "allowed": "Permitido", + "topCountries": "Top Países", "accessRoleSelect": "Seleccionar rol", "inviteEmailSentDescription": "Se ha enviado un correo electrónico al usuario con el siguiente enlace de acceso. Debe acceder al enlace para aceptar la invitación.", "inviteSentDescription": "El usuario ha sido invitado. Debe acceder al enlace de abajo para aceptar la invitación.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Guardar controles de acceso", "roles": "Roles", "accessUsersRoles": "Administrar usuarios y roles", - "accessUsersRolesDescription": "Invitar usuarios y añadirlos a roles para administrar el acceso a su organización", + "accessUsersRolesDescription": "Invitar usuarios y añadirlos a roles para administrar el acceso a la organización", "key": "Clave", "createdAt": "Creado el", "proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.", "proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.", "proxyEnableSSL": "Activar SSL", - "proxyEnableSSLDescription": "Activa el cifrado SSL/TLS para conexiones seguras HTTPS a tus objetivos.", + "proxyEnableSSLDescription": "Habilita el cifrado SSL/TLS para conexiones seguras HTTPS a los objetivos.", "target": "Target", "configureTarget": "Configurar objetivos", "targetErrorFetch": "Error al recuperar los objetivos", @@ -480,24 +492,24 @@ "targetsErrorUpdate": "Error al actualizar los objetivos", "targetsErrorUpdateDescription": "Se ha producido un error al actualizar los objetivos", "targetTlsUpdate": "Ajustes TLS actualizados", - "targetTlsUpdateDescription": "La configuración de TLS se ha actualizado correctamente", + "targetTlsUpdateDescription": "Los ajustes de TLS se han actualizado correctamente", "targetErrorTlsUpdate": "Error al actualizar los ajustes de TLS", "targetErrorTlsUpdateDescription": "Ocurrió un error mientras se actualizaban los ajustes de TLS", "proxyUpdated": "Configuración del proxy actualizada", "proxyUpdatedDescription": "La configuración del proxy se ha actualizado correctamente", "proxyErrorUpdate": "Error al actualizar la configuración del proxy", "proxyErrorUpdateDescription": "Se ha producido un error al actualizar la configuración del proxy", - "targetAddr": "IP / Nombre del host", + "targetAddr": "Anfitrión", "targetPort": "Puerto", "targetProtocol": "Protocolo", "targetTlsSettings": "Configuración de conexión segura", - "targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para su recurso", + "targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para el recurso", "targetTlsSettingsAdvanced": "Ajustes avanzados de TLS", "targetTlsSni": "Nombre del servidor TLS", "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": "Establecer objetivos para enrutar tráfico a servicios de backend", "targetStickySessions": "Activar Sesiones Pegadas", "targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.", "methodSelect": "Seleccionar método", @@ -516,9 +528,11 @@ "targetCreatedDescription": "El objetivo se ha creado correctamente", "targetErrorCreate": "Error al crear el objetivo", "targetErrorCreateDescription": "Se ha producido un error al crear el objetivo", + "tlsServerName": "Nombre del servidor TLS", + "tlsServerNameDescription": "El nombre del servidor TLS a usar para SNI", "save": "Guardar", "proxyAdditional": "Ajustes adicionales del proxy", - "proxyAdditionalDescription": "Configura cómo tu recurso maneja la configuración del proxy", + "proxyAdditionalDescription": "Configurar cómo maneja el recurso la configuración del proxy", "proxyCustomHeader": "Cabecera de host personalizada", "proxyCustomHeaderDescription": "La cabecera del host a establecer cuando se realizan peticiones de reemplazo. Deje en blanco para usar el valor predeterminado.", "proxyAdditionalSubmit": "Guardar ajustes de proxy", @@ -558,7 +572,7 @@ "rulesMatchType": "Tipo de partida", "value": "Valor", "rulesAbout": "Sobre Reglas", - "rulesAboutDescription": "Las reglas le permiten controlar el acceso a su recurso basado en un conjunto de criterios. Puede crear reglas para permitir o denegar el acceso basándose en la dirección IP o ruta de la URL.", + "rulesAboutDescription": "Las reglas permiten controlar el acceso al recurso basado en un conjunto de criterios. Puede crear reglas para permitir o denegar el acceso basándose en la dirección IP o ruta de la URL.", "rulesActions": "Acciones", "rulesActionAlwaysAllow": "Permitir siempre: pasar todos los métodos de autenticación", "rulesActionAlwaysDeny": "Denegar siempre: Bloquear todas las peticiones; no se puede intentar autenticación", @@ -570,7 +584,7 @@ "rulesEnable": "Activar Reglas", "rulesEnableDescription": "Activar o desactivar la evaluación de reglas para este recurso", "rulesResource": "Configuración de reglas de recursos", - "rulesResourceDescription": "Configurar reglas para controlar el acceso a su recurso", + "rulesResourceDescription": "Configurar reglas para controlar el acceso al recurso", "ruleSubmit": "Añadir Regla", "rulesNoOne": "No hay reglas. Agregue una regla usando el formulario.", "rulesOrder": "Las reglas son evaluadas por prioridad en orden ascendente.", @@ -586,7 +600,7 @@ "none": "Ninguna", "unknown": "Desconocido", "resources": "Recursos", - "resourcesDescription": "Los recursos son proxies para aplicaciones que se ejecutan en su red privada. Cree un recurso para cualquier servicio HTTP/HTTPS o TCP/UDP crudo en su red privada. Cada recurso debe estar conectado a un sitio para permitir una conectividad privada y segura a través de un túnel encriptado de WireGuard.", + "resourcesDescription": "Los recursos son proxies a las aplicaciones que se ejecutan en la red privada. Crea un recurso para cualquier servicio HTTP/HTTPS o TCP/UDP crudo en tu red privada. Cada recurso debe estar conectado a un sitio para permitir una conectividad privada y segura a través de un túnel encriptado de WireGuard.", "resourcesWireGuardConnect": "Conectividad segura con cifrado de Wirex Guard", "resourcesMultipleAuthenticationMethods": "Configurar múltiples métodos de autenticación", "resourcesUsersRolesAccess": "Control de acceso basado en usuarios y roles", @@ -597,7 +611,7 @@ "resourceSelect": "Seleccionar recurso", "shareLinks": "Compartir enlaces", "share": "Enlaces compartibles", - "shareDescription2": "Crea enlaces compartidos con tus recursos. Los enlaces proporcionan acceso temporal o ilimitado a tu recurso. Puede configurar la duración de caducidad del enlace cuando cree uno.", + "shareDescription2": "Crea enlaces compartidos a recursos. Los enlaces proporcionan acceso temporal o ilimitado a tu recurso. Puede configurar la duración de caducidad del enlace cuando cree uno.", "shareEasyCreate": "Fácil de crear y compartir", "shareConfigurableExpirationDuration": "Duración de caducidad configurable", "shareSecureAndRevocable": "Seguro y revocable", @@ -607,19 +621,19 @@ "unknownCommand": "Comando desconocido", "newtErrorFetchReleases": "No se pudo obtener la información del lanzamiento: {err}", "newtErrorFetchLatest": "Error obteniendo la última versión: {err}", - "newtEndpoint": "Punto final de Newt", - "newtId": "ID de Newt", - "newtSecretKey": "Clave secreta de Newt", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Secreto", "architecture": "Arquitectura", "sites": "Sitios", - "siteWgAnyClients": "Usa cualquier cliente de Wirex para conectarte. Tendrás que dirigirte a tus recursos internos usando la IP de compañeros.", + "siteWgAnyClients": "Usa cualquier cliente de Wirex para conectarte. Tendrás que dirigirte a los recursos internos usando la IP de compañeros.", "siteWgCompatibleAllClients": "Compatible con todos los clientes de Wirex Guard", "siteWgManualConfigurationRequired": "Configuración manual requerida", "userErrorNotAdminOrOwner": "El usuario no es un administrador o propietario", "pangolinSettings": "Ajustes - Pangolin", "accessRoleYour": "Tu rol:", - "accessRoleSelect2": "Seleccione un rol", - "accessUserSelect": "Seleccione un usuario", + "accessRoleSelect2": "Seleccionar roles", + "accessUserSelect": "Seleccionar usuarios", "otpEmailEnter": "Escribe un email", "otpEmailEnterDescription": "Pulse Enter para añadir un correo electrónico después de teclearlo en el campo de entrada.", "otpEmailErrorInvalid": "Dirección de correo electrónico no válida. El comodín (*) debe ser la parte local completa.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Definir Pincode", "resourcePincodeSetupTitleDescription": "Establecer un pincode para proteger este recurso", "resourceRoleDescription": "Los administradores siempre pueden acceder a este recurso.", - "resourceUsersRoles": "Usuarios y roles", + "resourceUsersRoles": "Controles de acceso", "resourceUsersRolesDescription": "Configurar qué usuarios y roles pueden visitar este recurso", "resourceUsersRolesSubmit": "Guardar usuarios y roles", "resourceWhitelistSave": "Guardado correctamente", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Transferir recursos", "siteDestination": "Sitio de destino", "searchSites": "Buscar sitios", + "countries": "Países", "accessRoleCreate": "Crear rol", "accessRoleCreateDescription": "Crear un nuevo rol para agrupar usuarios y administrar sus permisos.", "accessRoleCreateSubmit": "Crear rol", @@ -766,15 +781,15 @@ "idpOidcConfigure": "Configuración OAuth2/OIDC", "idpOidcConfigureDescription": "Configurar los puntos finales y credenciales del proveedor OAuth2/OIDC", "idpClientId": "ID de cliente", - "idpClientIdDescription": "El ID del cliente OAuth2 de su proveedor de identidad", + "idpClientIdDescription": "El ID del cliente OAuth2 del proveedor de identidad", "idpClientSecret": "Cliente secreto", - "idpClientSecretDescription": "El secreto del cliente OAuth2 de su proveedor de identidad", + "idpClientSecretDescription": "El secreto del cliente OAuth2 del proveedor de identidad", "idpAuthUrl": "URL de autorización", "idpAuthUrlDescription": "La URL final de autorización de OAuth2", "idpTokenUrl": "URL del token", "idpTokenUrlDescription": "La URL del endpoint del token OAuth2", "idpOidcConfigureAlert": "Información importante", - "idpOidcConfigureAlertDescription": "Después de crear el proveedor de identidad, necesitará configurar la URL de callback en la configuración de su proveedor de identidad. La URL de devolución de llamada se proporcionará después de la creación exitosa.", + "idpOidcConfigureAlertDescription": "Después de crear el proveedor de identidad, necesitará configurar la URL de callback en la configuración del proveedor de identidad. La URL de devolución de llamada se proporcionará después de la creación exitosa.", "idpToken": "Configuración del token", "idpTokenDescription": "Configurar cómo extraer la información del usuario del token de ID", "idpJmespathAbout": "Acerca de JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Crear proveedor de identidad", "orgPolicies": "Políticas de organización", "idpSettings": "Ajustes {idpName}", - "idpCreateSettingsDescription": "Configurar la configuración de su proveedor de identidad", + "idpCreateSettingsDescription": "Configurar la configuración del proveedor de identidad", "roleMapping": "Mapeo de Rol", "orgMapping": "Mapeo de organización", "orgPoliciesSearch": "Buscar políticas de organización...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Proveedor de identidad actualizado correctamente", "redirectUrl": "URL de redirección", "redirectUrlAbout": "Acerca de la URL de redirección", - "redirectUrlAboutDescription": "Esta es la URL a la que los usuarios serán redireccionados después de la autenticación. Necesitas configurar esta URL en la configuración de tu proveedor de identidad.", + "redirectUrlAboutDescription": "Esta es la URL a la que los usuarios serán redireccionados después de la autenticación. Necesitas configurar esta URL en la configuración del proveedor de identidad.", "pangolinAuth": "Autenticación - Pangolin", "verificationCodeLengthRequirements": "Tu código de verificación debe tener 8 caracteres.", "errorOccurred": "Se ha producido un error", @@ -909,6 +924,10 @@ "passwordResetSent": "Enviaremos un código para restablecer la contraseña a esta dirección de correo electrónico.", "passwordResetCode": "Código de restablecimiento", "passwordResetCodeDescription": "Revisa tu correo electrónico para ver el código de restablecimiento.", + "generatePasswordResetCode": "Generar código de restablecimiento de contraseña", + "passwordResetCodeGenerated": "Código de restablecimiento de contraseña generado", + "passwordResetCodeGeneratedDescription": "Comparte este código con el usuario. Pueden usarlo para restablecer su contraseña.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nueva contraseña", "passwordNewConfirm": "Confirmar nueva contraseña", "changePassword": "Cambiar Contraseña", @@ -926,6 +945,9 @@ "pincodeAuth": "Código de autenticación", "pincodeSubmit2": "Enviar código", "passwordResetSubmit": "Reiniciar Solicitud", + "passwordResetAlreadyHaveCode": "Introduzca el código de restablecimiento de contraseña", + "passwordResetSmtpRequired": "Póngase en contacto con su administrador", + "passwordResetSmtpRequiredDescription": "Se requiere un código de restablecimiento de contraseña para restablecer su contraseña. Póngase en contacto con su administrador para obtener asistencia.", "passwordBack": "Volver a la contraseña", "loginBack": "Volver a iniciar sesión", "signup": "Regístrate", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Listar recursos del sitio", "actionUpdateSiteResource": "Actualizar recurso del sitio", "actionListInvitations": "Listar invitaciones", + "actionExportLogs": "Exportar registros", + "actionViewLogs": "Ver registros", "noneSelected": "Ninguno seleccionado", "orgNotFound2": "No se encontraron organizaciones.", "searchProgress": "Buscar...", "create": "Crear", "orgs": "Organizaciones", "loginError": "Se ha producido un error al iniciar sesión", + "loginRequiredForDevice": "Es necesario iniciar sesión para autenticar tu dispositivo.", "passwordForgot": "¿Olvidaste tu contraseña?", "otpAuth": "Autenticación de dos factores", "otpAuthDescription": "Introduzca el código de su aplicación de autenticación o uno de sus códigos de copia de seguridad de un solo uso.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Inicio", "sidebarSites": "Sitios", "sidebarResources": "Recursos", + "sidebarProxyResources": "Público", + "sidebarClientResources": "Privado", "sidebarAccessControl": "Control de acceso", + "sidebarLogsAndAnalytics": "Registros y análisis", "sidebarUsers": "Usuarios", + "sidebarAdmin": "Admin", "sidebarInvitations": "Invitaciones", "sidebarRoles": "Roles", - "sidebarShareableLinks": "Enlaces compartibles", + "sidebarShareableLinks": "Enlaces", "sidebarApiKeys": "Claves API", "sidebarSettings": "Ajustes", "sidebarAllUsers": "Todos los usuarios", "sidebarIdentityProviders": "Proveedores de identidad", "sidebarLicense": "Licencia", "sidebarClients": "Clientes", + "sidebarUserDevices": "Usuarios", + "sidebarMachineClients": "Máquinas", "sidebarDomains": "Dominios", + "sidebarGeneral": "General", + "sidebarLogAndAnalytics": "Registro y análisis", "sidebarBluePrints": "Planos", + "sidebarOrganization": "Organización", + "sidebarLogsAnalytics": "Analíticas", "blueprints": "Planos", "blueprintsDescription": "Aplicar configuraciones declarativas y ver ejecuciones anteriores", "blueprintAdd": "Añadir plano", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Ver el resultado del plano aplicado y cualquier error que haya ocurrido", "blueprintInfo": "Información del plano", "message": "Mensaje", - "blueprintContentsDescription": "Defina el contenido YAML describiendo su infraestructura", + "blueprintContentsDescription": "Definir el contenido YAML describiendo la infraestructura", "blueprintErrorCreateDescription": "Se ha producido un error al aplicar el plano", "blueprintErrorCreate": "Error al crear el plano", "searchBlueprintProgress": "Buscar planos...", @@ -1230,15 +1265,15 @@ "loading": "Cargando", "restart": "Reiniciar", "domains": "Dominios", - "domainsDescription": "Administrar dominios de tu organización", + "domainsDescription": "Crear y administrar dominios disponibles en la organización", "domainsSearch": "Buscar dominios...", "domainAdd": "Agregar dominio", - "domainAddDescription": "Registrar un nuevo dominio con tu organización", + "domainAddDescription": "Registrar un nuevo dominio con la organización", "domainCreate": "Crear dominio", "domainCreatedDescription": "Dominio creado con éxito", "domainDeletedDescription": "Dominio eliminado exitosamente", - "domainQuestionRemove": "¿Está seguro que desea eliminar el dominio de su cuenta?", - "domainMessageRemove": "Una vez eliminado, el dominio ya no estará asociado con su cuenta.", + "domainQuestionRemove": "¿Está seguro que desea eliminar el dominio?", + "domainMessageRemove": "Una vez eliminado, el dominio ya no estará asociado a la organización.", "domainConfirmDelete": "Confirmar eliminación del dominio", "domainDelete": "Eliminar dominio", "domain": "Dominio", @@ -1257,7 +1292,7 @@ "pending": "Pendiente", "sidebarBilling": "Facturación", "billing": "Facturación", - "orgBillingDescription": "Gestiona tu información de facturación y suscripciones", + "orgBillingDescription": "Administrar información de facturación y suscripciones", "github": "GitHub", "pangolinHosted": "Pangolin Alojado", "fossorial": "Fossorial", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Actualizaciones de producto", "productUpdateEmpty": "Sin actualizaciones", "dismissAll": "Descartar todo", - "pangolinUpdateAvailable": "Nueva versión disponible", + "pangolinUpdateAvailable": "Actualización disponible", "pangolinUpdateAvailableInfo": "La versión {version} está lista para instalar", - "pangolinUpdateAvailableReleaseNotes": "Ver notas del lanzamiento", + "pangolinUpdateAvailableReleaseNotes": "Ver notas de lanzamiento", "newtUpdateAvailable": "Nueva actualización disponible", "newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.", "domainPickerEnterDomain": "Dominio", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Comprobando disponibilidad...", - "domainPickerNoMatchingDomains": "No se encontraron dominios que coincidan. Intente con un dominio diferente o verifique la configuración de dominios de su organización.", + "domainPickerNoMatchingDomains": "No se han encontrado dominios coincidentes. Prueba un dominio diferente o comprueba la configuración de dominio de la organización.", "domainPickerOrganizationDomains": "Dominios de la organización", "domainPickerProvidedDomains": "Dominios proporcionados", "domainPickerSubdomain": "Subdominio: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Modificar Suscripción", "billingStartSubscription": "Iniciar Suscripción", "billingRecurringCharge": "Cargo Recurrente", - "billingManageSubscriptionSettings": "Administra la configuración y preferencias de tu suscripción", + "billingManageSubscriptionSettings": "Administrar ajustes y preferencias de suscripción", "billingNoActiveSubscription": "No tienes una suscripción activa. Inicia tu suscripción para aumentar los límites de uso.", "billingFailedToLoadSubscription": "Error al cargar la suscripción", "billingFailedToLoadUsage": "Error al cargar el uso", @@ -1345,9 +1380,9 @@ "billingPortalError": "Error del portal", "billingDataUsageInfo": "Se le cobran todos los datos transferidos a través de sus túneles seguros cuando se conectan a la nube. Esto incluye tanto tráfico entrante como saliente a través de todos sus sitios. Cuando alcance su límite, sus sitios se desconectarán hasta que actualice su plan o reduzca el uso. Los datos no se cargan cuando se usan nodos.", "billingOnlineTimeInfo": "Se te cobrará en función del tiempo que tus sitios permanezcan conectados a la nube. Por ejemplo, 44.640 minutos equivale a un sitio que funciona 24/7 durante un mes completo. Cuando alcance su límite, sus sitios se desconectarán hasta que mejore su plan o reduzca el uso. No se cargará el tiempo al usar nodos.", - "billingUsersInfo": "Se te cobra por cada usuario en tu organización. La facturación se calcula diariamente según la cantidad de cuentas de usuario activas en tu organización.", - "billingDomainInfo": "Se te cobra por cada dominio en tu organización. La facturación se calcula diariamente según la cantidad de cuentas de dominio activas en tu organización.", - "billingRemoteExitNodesInfo": "Se te cobra por cada nodo gestionado en tu organización. La facturación se calcula diariamente según la cantidad de nodos gestionados activos en tu organización.", + "billingUsersInfo": "Se le cobra por cada usuario en la organización. La facturación se calcula diariamente según el número de cuentas de usuario activas en su órgano.", + "billingDomainInfo": "Se le cobra por cada dominio en la organización. La facturación se calcula diariamente en función del número de cuentas de dominio activas en su órgano.", + "billingRemoteExitNodesInfo": "Se le cobra por cada nodo administrado en la organización. La facturación se calcula diariamente en función del número de nodos activos gestionados en su órgano.", "domainNotFound": "Dominio no encontrado", "domainNotFoundDescription": "Este recurso está deshabilitado porque el dominio ya no existe en nuestro sistema. Por favor, establece un nuevo dominio para este recurso.", "failed": "Fallido", @@ -1430,29 +1465,32 @@ "and": "y", "privacyPolicy": "política de privacidad" }, + "signUpMarketing": { + "keepMeInTheLoop": "Mantenerme en el bucle con noticias, actualizaciones y nuevas características por correo electrónico." + }, "siteRequired": "El sitio es requerido.", "olmTunnel": "Túnel Olm", "olmTunnelDescription": "Usar Olm para la conectividad del cliente", "errorCreatingClient": "Error al crear el cliente", "clientDefaultsNotFound": "Configuración predeterminada del cliente no encontrada", "createClient": "Crear cliente", - "createClientDescription": "Crear un cliente nuevo para conectar a sus sitios", + "createClientDescription": "Crear un nuevo cliente para acceder a recursos privados", "seeAllClients": "Ver todos los clientes", "clientInformation": "Información del cliente", "clientNamePlaceholder": "Nombre del cliente", "address": "Dirección", "subnetPlaceholder": "Subred", - "addressDescription": "La dirección que este cliente utilizará para la conectividad", + "addressDescription": "La dirección interna del cliente. Debe estar dentro de la subred de la organización.", "selectSites": "Seleccionar sitios", "sitesDescription": "El cliente tendrá conectividad con los sitios seleccionados", "clientInstallOlm": "Instalar Olm", "clientInstallOlmDescription": "Obtén Olm funcionando en tu sistema", - "clientOlmCredentials": "Credenciales Olm", - "clientOlmCredentialsDescription": "Así es como Olm se autentificará con el servidor", - "olmEndpoint": "Punto final Olm", - "olmId": "ID de Olm", - "olmSecretKey": "Clave secreta de Olm", - "clientCredentialsSave": "Guarda tus credenciales", + "clientOlmCredentials": "Credenciales", + "clientOlmCredentialsDescription": "Así es como el cliente se autentificará con el servidor", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Secreto", + "clientCredentialsSave": "Guardar las credenciales", "clientCredentialsSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.", "generalSettingsDescription": "Configura la configuración general para este cliente", "clientUpdated": "Cliente actualizado", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Se ha producido un error al recuperar los sitios.", "olmErrorFetchReleases": "Se ha producido un error al recuperar las versiones de Olm.", "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 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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Controlar la salud de este objetivo. Puedes supervisar un punto final diferente al objetivo si es necesario.", "healthScheme": "Método", "healthSelectScheme": "Seleccionar método", + "healthCheckPortInvalid": "El puerto de chequeo de salud debe estar entre 1 y 65535", "healthCheckPath": "Ruta", "healthHostname": "IP / Nombre del host", "healthPort": "Puerto", "healthCheckPathDescription": "La ruta para comprobar el estado de salud.", - "healthyIntervalSeconds": "Intervalo Saludable", - "unhealthyIntervalSeconds": "Intervalo No Saludable", + "healthyIntervalSeconds": "Intervalo saludable (seg)", + "unhealthyIntervalSeconds": "Intervalo poco saludable (seg)", "IntervalSeconds": "Intervalo Saludable", - "timeoutSeconds": "Tiempo de Espera", + "timeoutSeconds": "Tiempo agotado (seg)", "timeIsInSeconds": "El tiempo está en segundos", "retryAttempts": "Intentos de Reintento", "expectedResponseCodes": "Códigos de respuesta esperados", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Editar dominio", "siteName": "Nombre del sitio", "proxyPort": "Puerto", - "resourcesTableProxyResources": "Recursos de proxy", - "resourcesTableClientResources": "Recursos del cliente", + "resourcesTableProxyResources": "Público", + "resourcesTableClientResources": "Privado", "resourcesTableNoProxyResourcesFound": "No se encontraron recursos de proxy.", "resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.", "resourcesTableDestination": "Destino", - "resourcesTableTheseResourcesForUseWith": "Estos recursos son para uso con", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Clientes", "resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.", "resourcesTableNoTargets": "Sin objetivos", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Desconectado", "resourcesTableUnknown": "Desconocido", "resourcesTableNotMonitored": "No supervisado", - "editInternalResourceDialogEditClientResource": "Editar recurso del cliente", - "editInternalResourceDialogUpdateResourceProperties": "Actualizar las propiedades del recurso y la configuración del objetivo para {resourceName}.", + "editInternalResourceDialogEditClientResource": "Editar recurso privado", + "editInternalResourceDialogUpdateResourceProperties": "Actualizar la configuración del recurso y los controles de acceso para {resourceName}", "editInternalResourceDialogResourceProperties": "Propiedades del recurso", "editInternalResourceDialogName": "Nombre", "editInternalResourceDialogProtocol": "Protocolo", @@ -1560,11 +1597,22 @@ "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", + "editInternalResourceDialogPortModeRequired": "Protocolos, puerto proxy y puerto de destino son necesarios para el modo puerto", + "editInternalResourceDialogMode": "Modo", + "editInternalResourceDialogModePort": "Puerto", + "editInternalResourceDialogModeHost": "Anfitrión", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Destino", + "editInternalResourceDialogDestinationHostDescription": "La dirección IP o nombre de host del recurso en la red del sitio.", + "editInternalResourceDialogDestinationIPDescription": "La dirección IP o nombre de host del recurso en la red del sitio.", + "editInternalResourceDialogDestinationCidrDescription": "El rango CIDR del recurso en la red del sitio.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Un alias DNS interno opcional para este recurso.", "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.", + "createInternalResourceDialogCreateClientResource": "Crear recurso privado", + "createInternalResourceDialogCreateClientResourceDescription": "Crear un nuevo recurso que sólo será accesible a los clientes conectados a la organización", "createInternalResourceDialogResourceProperties": "Propiedades del recurso", "createInternalResourceDialogName": "Nombre", "createInternalResourceDialogSite": "Sitio", @@ -1593,11 +1641,22 @@ "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", + "createInternalResourceDialogPortModeRequired": "Protocolos, puerto proxy y puerto de destino son necesarios para el modo puerto", + "createInternalResourceDialogMode": "Modo", + "createInternalResourceDialogModePort": "Puerto", + "createInternalResourceDialogModeHost": "Anfitrión", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Destino", + "createInternalResourceDialogDestinationHostDescription": "La dirección IP o nombre de host del recurso en la red del sitio.", + "createInternalResourceDialogDestinationCidrDescription": "El rango CIDR del recurso en la red del sitio.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Un alias DNS interno opcional para este recurso.", "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.", + "siteAcceptClientConnectionsDescription": "Permitir a los dispositivos de usuario y clientes acceder a los recursos de este sitio. Esto se puede cambiar más tarde.", + "siteAddress": "Dirección del sitio (Avanzado)", + "siteAddressDescription": "La dirección interna del sitio. Debe estar dentro de la subred de la organización.", + "siteNameDescription": "El nombre mostrado del sitio que se puede cambiar más adelante.", "autoLoginExternalIdp": "Inicio de sesión automático con IDP externo", "autoLoginExternalIdpDescription": "Redirigir inmediatamente al usuario al IDP externo para autenticación.", "selectIdp": "Seleccionar IDP", @@ -1627,7 +1686,7 @@ "viewAllButton": "Ver todos los nodos", "strategy": { "title": "Estrategia de Creación", - "description": "Elija esto para configurar manualmente su nodo o generar nuevas credenciales.", + "description": "Elija esto para configurar manualmente el nodo o generar nuevas credenciales.", "adopt": { "title": "Adoptar Nodo", "description": "Elija esto si ya tiene las credenciales para el nodo." @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Credenciales Generadas", - "description": "Utilice estas credenciales generadas para configurar su nodo", + "description": "Utilice estas credenciales generadas para configurar el nodo", "nodeIdTitle": "ID del nodo", "secretTitle": "Secreto", "saveCredentialsTitle": "Agregar Credenciales a la Configuración", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Tipo de proveedor de identidad", "roleMappingExpressionPlaceholder": "e.g., contiene(grupos, 'administrador') && 'administrador' || 'miembro'", "idpGoogleConfiguration": "Configuración de Google", - "idpGoogleConfigurationDescription": "Configura tus credenciales de Google OAuth2", - "idpGoogleClientIdDescription": "Tu ID de cliente de Google OAuth2", - "idpGoogleClientSecretDescription": "Tu secreto de cliente de Google OAuth2", + "idpGoogleConfigurationDescription": "Configurar las credenciales de Google OAuth2", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Secreto del cliente de Google OAuth2", "idpAzureConfiguration": "Configuración de Azure Entra ID", - "idpAzureConfigurationDescription": "Configure sus credenciales de Azure Entra ID OAuth2", + "idpAzureConfigurationDescription": "Configurar credenciales de Azure Entra ID OAuth2", "idpTenantId": "ID del inquilino", - "idpTenantIdPlaceholder": "su-inquilino-id", - "idpAzureTenantIdDescription": "Su ID de inquilino de Azure (encontrado en el resumen de Azure Active Directory)", - "idpAzureClientIdDescription": "Tu ID de Cliente de Registro de Azure App", - "idpAzureClientSecretDescription": "Tu Azure App Registro Cliente secreto", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "ID de inquilino Azure (encontrado en la descripción de Azure Active Directory)", + "idpAzureClientIdDescription": "ID de cliente de registro de Azure App", + "idpAzureClientSecretDescription": "Azure App Registro Cliente secreto", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Configuración de Google", "idpAzureConfigurationTitle": "Configuración de Azure Entra ID", "idpTenantIdLabel": "ID del inquilino", - "idpAzureClientIdDescription2": "Tu ID de Cliente de Registro de Azure App", - "idpAzureClientSecretDescription2": "Tu Azure App Registro Cliente secreto", + "idpAzureClientIdDescription2": "ID de cliente de registro de Azure App", + "idpAzureClientSecretDescription2": "Azure App Registro Cliente secreto", "idpGoogleDescription": "Proveedor OAuth2/OIDC de Google", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Subred", "subnetDescription": "La subred para la configuración de red de esta organización.", "authPage": "Página Auth", - "authPageDescription": "Configurar la página de autenticación de su organización", + "authPageDescription": "Configurar la página de autenticación para la organización", "authPageDomain": "Dominio de la página Auth", "noDomainSet": "Ningún dominio establecido", "changeDomain": "Cambiar dominio", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Establecer dominio Auth Page", "failedToFetchCertificate": "Error al obtener el certificado", "failedToRestartCertificate": "Error al reiniciar el certificado", - "addDomainToEnableCustomAuthPages": "Añadir un dominio para habilitar páginas de autenticación personalizadas para su organización", + "addDomainToEnableCustomAuthPages": "Añadir un dominio para habilitar páginas de autenticación personalizadas para la organización", "selectDomainForOrgAuthPage": "Seleccione un dominio para la página de autenticación de la organización", "domainPickerProvidedDomain": "Dominio proporcionado", "domainPickerFreeProvidedDomain": "Dominio proporcionado gratis", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "No se ha podido hacer válido \"{sub}\" para {domain}.", "domainPickerSubdomainSanitized": "Subdominio saneado", "domainPickerSubdomainCorrected": "\"{sub}\" fue corregido a \"{sanitized}\"", - "orgAuthSignInTitle": "Inicia sesión en tu organización", + "orgAuthSignInTitle": "Iniciar sesión en la organización", "orgAuthChooseIdpDescription": "Elige tu proveedor de identidad para continuar", "orgAuthNoIdpConfigured": "Esta organización no tiene ningún proveedor de identidad configurado. En su lugar puedes iniciar sesión con tu identidad de Pangolin.", "orgAuthSignInWithPangolin": "Iniciar sesión con Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Habilitar autenticación de doble factor", "completeSecuritySteps": "Pasos de seguridad completos", "securitySettings": "Ajustes de seguridad", - "securitySettingsDescription": "Configurar políticas de seguridad para su organización", + "securitySettingsDescription": "Configurar políticas de seguridad para la organización", "requireTwoFactorForAllUsers": "Requiere autenticación de doble factor para todos los usuarios", "requireTwoFactorDescription": "Cuando está activado, todos los usuarios internos de esta organización deben tener habilitada la autenticación de dos factores para acceder a la organización.", "requireTwoFactorDisabledDescription": "Esta característica requiere una licencia válida (Enterprise) o una suscripción activa (SaBudget)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Edición corporativa", "unlicensed": "Sin licencia", "beta": "Beta", - "manageClients": "Administrar clientes", - "manageClientsDescription": "Los clientes son dispositivos que pueden conectarse a sus sitios", + "manageUserDevices": "Dispositivos de usuario", + "manageUserDevicesDescription": "Ver y administrar dispositivos que los usuarios utilizan para conectarse a recursos privados", + "manageMachineClients": "Administrar clientes de máquinas", + "manageMachineClientsDescription": "Crear y administrar clientes que servidores y sistemas utilizan para conectarse de forma privada a recursos", + "clientsTableUserClients": "Usuario", + "clientsTableMachineClients": "Maquina", "licenseTableValidUntil": "Válido hasta", "saasLicenseKeysSettingsTitle": "Licencias empresariales", "saasLicenseKeysSettingsDescription": "Generar y administrar claves de licencia Enterprise para instancias Pangolin autoalojadas", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "clip", "sidebarEnableEnterpriseLicense": "Activar licencia corporativa", "cannotbeUndone": "Esto no se puede deshacer.", - "toConfirm": "confirmar", + "toConfirm": "confirmar.", "deleteClientQuestion": "¿Está seguro que desea eliminar el cliente del sitio y la organización?", "clientMessageRemove": "Una vez eliminado, el cliente ya no podrá conectarse al sitio.", "sidebarLogs": "Registros", "request": "Solicitud", + "requests": "Solicitudes", "logs": "Registros", "logsSettingsDescription": "Monitorear registros recogidos de esta orginización", "searchLogs": "Buscar registros...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Razón", "requestLogs": "Registros de Solicitud", + "requestAnalytics": "Analítica de Solicitud", "host": "Anfitrión", "location": "Ubicación", "actionLogs": "Registros de acción", @@ -2029,6 +2094,7 @@ "logRetention": "Retención de Log", "logRetentionDescription": "Administrar cuánto tiempo se conservan los diferentes tipos de registros para esta organización o desactivarlos", "requestLogsDescription": "Ver registros de solicitudes detallados para los recursos de esta organización", + "requestAnalyticsDescription": "Ver análisis de solicitudes detalladas de recursos en esta organización", "logRetentionRequestLabel": "Retención de Registro de Solicitud", "logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes", "logRetentionAccessLabel": "Retención de Log de Acceso", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 días", "logRetention90Days": "90 días", "logRetentionForever": "Para siempre", + "logRetentionEndOfFollowingYear": "Fin del año siguiente", "actionLogsDescription": "Ver un historial de acciones realizadas en esta organización", "accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización", "licenseRequiredToUse": "Se requiere una licencia Enterprise para utilizar esta función.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Certificado de comodín preferido", "unverified": "Sin verificar", "domainSetting": "Ajustes de dominio", - "domainSettingDescription": "Configurar ajustes para tu dominio", + "domainSettingDescription": "Configurar ajustes para el dominio", "preferWildcardCertDescription": "Intento de generar un certificado comodín (requiere una resolución de certificados correctamente configurada).", "recordName": "Nombre del registro", "auto": "Auto", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Una versión actualizada de Olm está disponible. Por favor, actualice a la última versión para obtener la mejor experiencia.", "client": "Cliente", "proxyProtocol": "Configuración del Protocolo Proxy", - "proxyProtocolDescription": "Configurar el protocolo de proxy para preservar las direcciones IP del cliente para los servicios TCP/UDP.", + "proxyProtocolDescription": "Configurar el protocolo de proxy para preservar las direcciones IP del cliente para los servicios TCP.", "enableProxyProtocol": "Habilitar protocolo proxy", - "proxyProtocolInfo": "Conservar direcciones IP del cliente para backends TCP/UDP", + "proxyProtocolInfo": "Conservar direcciones IP del cliente para backends TCP", "proxyProtocolVersion": "Versión del Protocolo Proxy", "version1": " Versión 1 (Recomendado)", "version2": "Versión 2", "versionDescription": "La versión 1 está basada en texto y es ampliamente soportada. La versión 2 es binaria y más eficiente pero menos compatible.", "warning": "Advertencia", - "proxyProtocolWarning": "Su aplicación de backend debe estar configurada para aceptar conexiones Proxy Protocol. Si su backend no soporta Proxy Protocol, habilitando esto romperá todas las conexiones. Asegúrese de configurar su backend para que confíe en las cabeceras del protocolo Proxy de Traefik.", + "proxyProtocolWarning": "La aplicación backend debe configurarse para aceptar conexiones Proxy Protocol. Si el backend no soporta Proxy Protocol, activarlo romperá todas las conexiones, así que sólo habilítelo si sabe lo que está haciendo. Asegúrese de configurar su backend para que confíe en las cabeceras del protocolo Proxy de Traefik.", "restarting": "Reiniciando...", "manual": "Manual", "messageSupport": "Soporte de mensajes", @@ -2097,6 +2164,43 @@ "supportMessageSent": "¡Mensaje enviado!", "supportWillContact": "¡Estaremos en contacto en breve!", "selectLogRetention": "Seleccionar retención de registro", + "terms": "Términos", + "privacy": "Privacidad", + "security": "Seguridad", + "docs": "Documentos", + "deviceActivation": "Activación del dispositivo", + "deviceCodeInvalidFormat": "El código debe tener 9 caracteres (por ejemplo, A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Código no válido o caducado", + "deviceCodeVerifyFailed": "Error al verificar el código del dispositivo", + "signedInAs": "Conectado como", + "deviceCodeEnterPrompt": "Introduzca el código mostrado en el dispositivo", + "continue": "Continuar", + "deviceUnknownLocation": "Ubicación desconocida", + "deviceAuthorizationRequested": "Esta autorización fue solicitada a {location} el {date}. Asegúrate de confiar en este dispositivo ya que tendrá acceso a la cuenta.", + "deviceLabel": "Dispositivo: {deviceName}", + "deviceWantsAccess": "quiere acceder a su cuenta", + "deviceExistingAccess": "Acceso existente:", + "deviceFullAccess": "Acceso total a tu cuenta", + "deviceOrganizationsAccess": "Acceso a todas las organizaciones a las que su cuenta tiene acceso", + "deviceAuthorize": "Autorizar a {applicationName}", + "deviceConnected": "¡Dispositivo conectado!", + "deviceAuthorizedMessage": "El dispositivo está autorizado para acceder a su cuenta.", + "pangolinCloud": "Nube de Pangolin", + "viewDevices": "Ver dispositivos", + "viewDevicesDescription": "Administra tus dispositivos conectados", + "noDevices": "No hay dispositivos", + "dateCreated": "Fecha de creación", + "unnamedDevice": "Dispositivo sin nombre", + "deviceQuestionRemove": "¿Está seguro que desea eliminar este dispositivo?", + "deviceMessageRemove": "Esta acción no se puede deshacer.", + "deviceDeleteConfirm": "Eliminar dispositivo", + "deleteDevice": "Eliminar dispositivo", + "errorLoadingDevices": "Error al cargar dispositivos", + "failedToLoadDevices": "Error al cargar dispositivos", + "deviceDeleted": "Dispositivo eliminado", + "deviceDeletedDescription": "El dispositivo se ha eliminado correctamente.", + "errorDeletingDevice": "Error al eliminar el dispositivo", + "failedToDeleteDevice": "Error al eliminar el dispositivo", "showColumns": "Mostrar columnas", "hideColumns": "Ocultar columnas", "columnVisibility": "Visibilidad de la columna", @@ -2111,10 +2215,14 @@ "enableSelected": "Habilitar seleccionados", "disableSelected": "Desactivar Seleccionado", "checkSelectedStatus": "Comprobar el estado de selección", + "clients": "Clientes", + "accessClientSelect": "Seleccionar clientes de máquina", + "resourceClientDescription": "Clientes de máquina que pueden acceder a este recurso", + "regenerate": "Regenerar", "credentials": "Credenciales", "savecredentials": "Guardar credenciales", - "regeneratecredentials": "Re-clave", - "regenerateCredentials": "Regenerar y guardar tus credenciales", + "regenerateCredentialsButton": "Regenerar credenciales", + "regenerateCredentials": "Regenerar credenciales", "generatedcredentials": "Credenciales generadas", "copyandsavethesecredentials": "Copiar y guardar estas credenciales", "copyandsavethesecredentialsdescription": "Estas credenciales no se mostrarán de nuevo después de salir de esta página. Guárdelas de forma segura ahora.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Las credenciales se han regenerado y guardado correctamente.", "credentialsSaveError": "Error al guardar las credenciales", "credentialsSaveErrorDescription": "Se ha producido un error al regenerar y guardar las credenciales.", - "regenerateCredentialsWarning": "Regenerar las credenciales invalidará las anteriores. Asegúrese de actualizar cualquier configuración que use estas credenciales.", + "regenerateCredentialsWarning": "Regenerar las credenciales invalidará las anteriores y causará una desconexión. Asegúrese de actualizar cualquier configuración que use estas credenciales.", "confirm": "Confirmar", "regenerateCredentialsConfirmation": "¿Está seguro que desea regenerar las credenciales?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Clave secreta", - "featureDisabledTooltip": "Esta característica sólo está disponible en el plan empresarial y requiere una licencia para usarla.", "niceId": "ID bonita", "niceIdUpdated": "Bonito ID actualizado", "niceIdUpdatedSuccessfully": "Bonito ID actualizado correctamente", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Se ha producido un error al actualizar el ID de Niza.", "niceIdCannotBeEmpty": "El ID de Niza no puede estar vacío", "enterIdentifier": "Introducir identificador", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "¿No tú? Utilice una cuenta diferente.", + "deviceLoginDeviceRequestingAccessToAccount": "Un dispositivo está solicitando acceso a esta cuenta.", + "noData": "Sin datos", + "machineClients": "Clientes de la máquina", + "install": "Instalar", + "run": "Ejecutar", + "clientNameDescription": "El nombre mostrado del cliente que se puede cambiar más adelante.", + "clientAddress": "Dirección del cliente (Avanzado)", + "setupFailedToFetchSubnet": "No se pudo obtener la subred por defecto", + "setupSubnetAdvanced": "Subred (Avanzado)", + "setupSubnetDescription": "La subred de la red interna de esta organización.", + "siteRegenerateAndDisconnect": "Regenerar y desconectar", + "siteRegenerateAndDisconnectConfirmation": "¿Está seguro que desea regenerar las credenciales y desconectar este sitio?", + "siteRegenerateAndDisconnectWarning": "Esto regenerará las credenciales y desconectará inmediatamente el sitio. El sitio tendrá que reiniciarse con las nuevas credenciales.", + "siteRegenerateCredentialsConfirmation": "¿Está seguro de que desea regenerar las credenciales de este sitio?", + "siteRegenerateCredentialsWarning": "Esto regenerará las credenciales. El sitio permanecerá conectado hasta que lo reinicie manualmente y utilice las nuevas credenciales.", + "clientRegenerateAndDisconnect": "Regenerar y desconectar", + "clientRegenerateAndDisconnectConfirmation": "¿Está seguro que desea regenerar las credenciales y desconectar este cliente?", + "clientRegenerateAndDisconnectWarning": "Esto regenerará las credenciales y desconectará inmediatamente al cliente. El cliente tendrá que reiniciarse con las nuevas credenciales.", + "clientRegenerateCredentialsConfirmation": "¿Está seguro que desea regenerar las credenciales para este cliente?", + "clientRegenerateCredentialsWarning": "Esto regenerará las credenciales. El cliente permanecerá conectado hasta que lo reinicie manualmente y utilice las nuevas credenciales.", + "remoteExitNodeRegenerateAndDisconnect": "Regenerar y desconectar", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "¿Estás seguro de que quieres regenerar las credenciales y desconectar este nodo de salida remoto?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Esto regenerará las credenciales y desconectará inmediatamente el nodo de salida remoto. El nodo de salida remoto tendrá que reiniciarse con las nuevas credenciales.", + "remoteExitNodeRegenerateCredentialsConfirmation": "¿Estás seguro de que quieres regenerar las credenciales para este nodo de salida remoto?", + "remoteExitNodeRegenerateCredentialsWarning": "Esto regenerará las credenciales. El nodo de salida remoto permanecerá conectado hasta que lo reinicie manualmente y utilice las nuevas credenciales.", + "agent": "Agente" } diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 276fa9bd..63e2b5d8 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1,12 +1,12 @@ { - "setupCreate": "Créez votre organisation, vos nœuds et vos ressources", + "setupCreate": "Créer l'organisation, le site et les ressources", "setupNewOrg": "Nouvelle organisation", "setupCreateOrg": "Créer une organisation", "setupCreateResources": "Créer des ressources", "setupOrgName": "Nom de l'organisation", - "orgDisplayName": "Ceci est le nom affiché de votre organisation.", + "orgDisplayName": "Ceci est le nom d'affichage de l'organisation.", "orgId": "ID de l'organisation", - "setupIdentifierMessage": "Ceci est l'identifiant unique de votre organisation. Il est différent du nom.", + "setupIdentifierMessage": "C'est l'identifiant unique de l'organisation.", "setupErrorIdentifier": "Cet ID est déjà utilisé. Veuillez en choisir un autre.", "componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.", "componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Une fois supprimé, le nœud ne sera plus accessible. Toutes les cibles associées au nœud seront également supprimées.", "siteQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce nœud de l'organisation ?", "siteManageSites": "Gérer les nœuds", - "siteDescription": "Autoriser la connexion à votre réseau via des tunnels sécurisés", + "siteDescription": "Créer et gérer des sites pour activer la connectivité aux réseaux privés", "siteCreate": "Créer un nœud", "siteCreateDescription2": "Suivez les étapes ci-dessous pour créer et connecter un nouveau nœud", - "siteCreateDescription": "Créez un nouveau nœud pour commencer à connecter vos ressources", + "siteCreateDescription": "Créer un nouveau site pour commencer à connecter des ressources", "close": "Fermer", "siteErrorCreate": "Erreur lors de la création du nœud", "siteErrorCreateKeyPair": "Clés ou nœud par défaut introuvable", @@ -74,7 +74,7 @@ "siteInstallNewt": "Installer Newt", "siteInstallNewtDescription": "Faites fonctionner Newt sur votre système", "WgConfiguration": "Configuration WireGuard", - "WgConfigurationDescription": "Utilisez la configuration suivante pour vous connecter à votre réseau", + "WgConfigurationDescription": "Utilisez la configuration suivante pour vous connecter au réseau", "operatingSystem": "Système d'exploitation", "commands": "Commandes", "recommended": "Recommandé", @@ -87,32 +87,32 @@ "siteUpdated": "Nœud mis à jour", "siteUpdatedDescription": "Le nœud a été mis à jour.", "siteGeneralDescription": "Configurer les paramètres par défaut de ce nœud", - "siteSettingDescription": "Configurer les paramètres de votre nœud", + "siteSettingDescription": "Configurer les paramètres du site", "siteSetting": "Paramètres de {siteName}", - "siteNewtTunnel": "Tunnel Newt (Recommandé)", - "siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.", + "siteNewtTunnel": "Site Newt (Recommandé)", + "siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans n'importe quel 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": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.", "siteLocalDescription": "Ressources locales seulement. Pas de tunneling.", "siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. Disponible uniquement sur les nœuds distants.", "siteSeeAll": "Voir tous les nœuds", - "siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre nœud", - "siteNewtCredentials": "Identifiants Newt", - "siteNewtCredentialsDescription": "C'est comme ça que Newt s'authentifiera avec le serveur", - "siteCredentialsSave": "Enregistrez vos identifiants", + "siteTunnelDescription": "Déterminer comment vous voulez vous connecter au site", + "siteNewtCredentials": "Identifiants", + "siteNewtCredentialsDescription": "Voici comment le site s'authentifiera avec le serveur", + "siteCredentialsSave": "Enregistrer les informations d'identification", "siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de l'enregistrer dans un endroit sécurisé.", "siteInfo": "Informations du nœud", "status": "Statut", "shareTitle": "Gérer les liens partageables", - "shareDescription": "Créez des liens partageables pour accorder un accès temporaire ou permanent à vos ressources", + "shareDescription": "Créez des liens partageables pour accorder un accès temporaire ou permanent aux ressources de proxy", "shareSearch": "Rechercher des liens partageables...", "shareCreate": "Créer un lien partageable", "shareErrorDelete": "Impossible de supprimer le lien", "shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien", "shareDeleted": "Lien supprimé", "shareDeletedDescription": "Le lien a été supprimé", - "shareTokenDescription": "Votre jeton d'accès peut être fourni de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Il doit être transmis par le client à chaque demande d'accès authentifié.", + "shareTokenDescription": "Le jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.", "accessToken": "Jeton d'accès", "usageExamples": "Exemples d'utilisation", "tokenId": "ID du jeton", @@ -121,7 +121,7 @@ "importantNote": "Note importante", "shareImportantDescription": "Pour des raisons de sécurité, l'utilisation des en-têtes est recommandée par rapport aux paramètres de la requête, dans la mesure du possible, car les paramètres de requête peuvent être enregistrés dans les journaux du serveur ou dans l'historique du navigateur.", "token": "Jeton", - "shareTokenSecurety": "Gardez votre jeton d'accès sécurisé. Ne le partagez pas dans des zones accessibles au public ou dans du code côté client.", + "shareTokenSecurety": "Gardez le jeton d'accès sécurisé. Ne le partagez pas dans des zones accessibles au public ou dans du code côté client.", "shareErrorFetchResource": "Impossible de récupérer les ressources", "shareErrorFetchResourceDescription": "Une erreur est survenue lors de la récupération des ressources", "shareErrorCreate": "Impossible de créer le lien partageable", @@ -144,8 +144,10 @@ "expires": "Expire", "never": "Jamais", "shareErrorSelectResource": "Veuillez sélectionner une ressource", - "resourceTitle": "Gérer les ressources", - "resourceDescription": "Créez des proxys sécurisés pour vos applications privées", + "proxyResourceTitle": "Gérer les ressources publiques", + "proxyResourceDescription": "Créer et gérer des ressources accessibles au public via un navigateur web", + "clientResourceTitle": "Gérer les ressources privées", + "clientResourceDescription": "Créer et gérer des ressources qui ne sont accessibles que via un client connecté", "resourcesSearch": "Chercher des ressources...", "resourceAdd": "Ajouter une ressource", "resourceErrorDelte": "Erreur lors de la de suppression de la ressource", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Une fois supprimée, la ressource ne sera plus accessible. Toutes les cibles associées à la ressource seront également supprimées.", "resourceQuestionRemove": "Êtes-vous sûr de vouloir retirer la ressource de l'organisation ?", "resourceHTTP": "Ressource HTTPS", - "resourceHTTPDescription": "Requêtes de proxy vers votre application via HTTPS en utilisant un sous-domaine ou un domaine racine.", + "resourceHTTPDescription": "Requêtes de proxy à l'application via HTTPS en utilisant un sous-domaine ou un domaine de base.", "resourceRaw": "Ressource TCP/UDP brute", - "resourceRawDescription": "Demandes de proxy vers votre application via TCP/UDP en utilisant un port.", + "resourceRawDescription": "Demandes de proxy à l'application via TCP/UDP en utilisant un numéro de port. Cela ne fonctionne que lorsque les sites sont connectés à des nœuds.", "resourceCreate": "Créer une ressource", "resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource", "resourceSeeAll": "Voir toutes les ressources", @@ -171,22 +173,22 @@ "noCountryFound": "Aucun pays trouvé.", "siteSelectionDescription": "Ce site fournira la connectivité à la cible.", "resourceType": "Type de ressource", - "resourceTypeDescription": "Détermine comment vous voulez accéder à votre ressource", + "resourceTypeDescription": "Déterminer comment accéder à la ressource", "resourceHTTPSSettings": "Paramètres HTTPS", - "resourceHTTPSSettingsDescription": "Configure comment votre ressource sera accédée via HTTPS", + "resourceHTTPSSettingsDescription": "Configurer comment la ressource sera accédée via HTTPS", "domainType": "Type de domaine", "subdomain": "Sous-domaine", "baseDomain": "Domaine racine", - "subdomnainDescription": "Le sous-domaine depuis lequel cette ressource sera accessible.", + "subdomnainDescription": "Le sous-domaine où la ressource sera accessible.", "resourceRawSettings": "Paramètres TCP/UDP", - "resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP. Vous mappez la ressource à un port sur le serveur Pangolin, de sorte que vous puissiez accéder à la ressource depuis ip-publique-du-serveur:port-mappé.", + "resourceRawSettingsDescription": "Configurer comment la ressource sera accédée via TCP/UDP", "protocol": "Protocole", "protocolSelect": "Choisir un protocole", "resourcePortNumber": "Numéro de port", "resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.", "cancel": "Abandonner", "resourceConfig": "Snippets de configuration", - "resourceConfigDescription": "Copiez et collez ces modules de configuration pour configurer votre ressource TCP/UDP", + "resourceConfigDescription": "Copiez et collez ces extraits de configuration pour configurer la ressource TCP/UDP", "resourceAddEntrypoints": "Traefik: Ajouter des points d'entrée", "resourceExposePorts": "Gerbil: Exposer des ports dans Docker Compose", "resourceLearnRaw": "Apprenez à configurer les ressources TCP/UDP", @@ -202,14 +204,14 @@ "proxy": "Proxy", "internal": "Interne", "rules": "Règles", - "resourceSettingDescription": "Configurer les paramètres de votre ressource", + "resourceSettingDescription": "Configurer les paramètres de la ressource", "resourceSetting": "Réglages de {resourceName}", - "alwaysAllow": "Toujours autoriser", - "alwaysDeny": "Toujours refuser", + "alwaysAllow": "Outrepasser l'authentification", + "alwaysDeny": "Bloquer l'accès", "passToAuth": "Passer à l'authentification", - "orgSettingsDescription": "Configurer les paramètres de votre organisation", + "orgSettingsDescription": "Configurer les paramètres de l'organisation", "orgGeneralSettings": "Paramètres de l'organisation", - "orgGeneralSettingsDescription": "Gérer les détails et la configuration de votre organisation", + "orgGeneralSettingsDescription": "Gérer les détails et la configuration de l'organisation", "saveGeneralSettings": "Enregistrer les paramètres généraux", "saveSettings": "Enregistrer les paramètres", "orgDangerZone": "Zone dangereuse", @@ -232,7 +234,7 @@ "orgMissing": "ID d'organisation manquant", "orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.", "accessUsersManage": "Gérer les utilisateurs", - "accessUsersDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à votre organisation", + "accessUsersDescription": "Inviter et gérer les utilisateurs ayant accès à cette organisation", "accessUsersSearch": "Chercher des utilisateurs...", "accessUserCreate": "Créer un utilisateur", "accessUserRemove": "Supprimer un utilisateur", @@ -241,13 +243,13 @@ "role": "Rôle", "nameRequired": "Le nom est requis", "accessRolesManage": "Gérer les rôles", - "accessRolesDescription": "Configurer les rôles pour gérer l'accès à votre organisation", + "accessRolesDescription": "Créer et gérer des rôles pour les utilisateurs de l'organisation", "accessRolesSearch": "Chercher des rôles...", "accessRolesAdd": "Ajouter un rôle", "accessRoleDelete": "Supprimer le rôle", "description": "Libellé", "inviteTitle": "Invitations actives", - "inviteDescription": "Gérez les invitations des autres utilisateurs", + "inviteDescription": "Gérer les invitations des autres utilisateurs à rejoindre l'organisation", "inviteSearch": "Rechercher des invitations...", "minutes": "Minutes", "hours": "Heures", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Erreur lors de la création de la clé API", "apiKeysErrorSetPermission": "Erreur lors de la définition des permissions", "apiKeysCreate": "Générer une clé d'API", - "apiKeysCreateDescription": "Générer une nouvelle clé d'API pour votre organisation", + "apiKeysCreateDescription": "Générer une nouvelle clé API pour l'organisation", "apiKeysGeneralSettings": "Permissions", "apiKeysGeneralSettingsDescription": "Déterminez ce que cette clé d\"API peut faire", - "apiKeysList": "Votre clé d\"API", - "apiKeysSave": "Enregistrer votre clé API", + "apiKeysList": "Nouvelle clé API", + "apiKeysSave": "Enregistrer la clé API", "apiKeysSaveDescription": "Vous ne pourrez la voir qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.", - "apiKeysInfo": "Votre clé d'API est :", + "apiKeysInfo": "La clé API est :", "apiKeysConfirmCopy": "J'ai copié la clé d\"API", "generate": "Générer", "done": "Terminé", @@ -424,7 +426,7 @@ "userCreated": "Utilisateur créé", "userCreatedDescription": "L'utilisateur a été créé avec succès.", "userTypeInternal": "Utilisateur interne", - "userTypeInternalDescription": "Inviter un utilisateur à rejoindre votre organisation directement.", + "userTypeInternalDescription": "Invitez un utilisateur à rejoindre l'organisation directement.", "userTypeExternal": "Utilisateur externe", "userTypeExternalDescription": "Créer un utilisateur avec un fournisseur d'identité externe.", "accessUserCreateDescription": "Suivez les étapes ci-dessous pour créer un nouvel utilisateur", @@ -436,6 +438,16 @@ "inviteEmailSent": "Envoyer un e-mail d'invitation à l'utilisateur", "inviteValid": "Valide pour", "selectDuration": "Sélectionner la durée", + "selectResource": "Sélectionner une ressource", + "filterByResource": "Filtrer par ressource", + "resetFilters": "Réinitialiser les filtres", + "totalBlocked": "Demandes bloquées par le Pangolin", + "totalRequests": "Total des demandes", + "requestsByCountry": "Requêtes par pays", + "requestsByDay": "Requêtes par jour", + "blocked": "Bloqué", + "allowed": "Autorisé", + "topCountries": "Meilleurs pays", "accessRoleSelect": "Sélectionner un rôle", "inviteEmailSentDescription": "Un e-mail a été envoyé à l'utilisateur avec le lien d'accès ci-dessous. Ils doivent accéder au lien pour accepter l'invitation.", "inviteSentDescription": "L'utilisateur a été invité. Ils doivent accéder au lien ci-dessous pour accepter l'invitation.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Enregistrer les contrôles d'accès", "roles": "Rôles", "accessUsersRoles": "Gérer les utilisateurs et les rôles", - "accessUsersRolesDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à votre organisation", + "accessUsersRolesDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à l'organisation", "key": "Clé", "createdAt": "Créé le", "proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.", "proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.", "proxyEnableSSL": "Activer SSL", - "proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.", + "proxyEnableSSLDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers les cibles.", "target": "Cible", "configureTarget": "Configurer les cibles", "targetErrorFetch": "Échec de la récupération des cibles", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Échec de la mise à jour des cibles", "targetsErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des cibles", "targetTlsUpdate": "Paramètres TLS mis à jour", - "targetTlsUpdateDescription": "Vos paramètres TLS ont été mis à jour avec succès", + "targetTlsUpdateDescription": "Les paramètres TLS ont été mis à jour avec succès", "targetErrorTlsUpdate": "Échec de la mise à jour des paramètres TLS", "targetErrorTlsUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres TLS", "proxyUpdated": "Paramètres du proxy mis à jour", - "proxyUpdatedDescription": "Vos paramètres de proxy ont été mis à jour avec succès", + "proxyUpdatedDescription": "Les paramètres du proxy ont été mis à jour avec succès", "proxyErrorUpdate": "Échec de la mise à jour des paramètres du proxy", "proxyErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres du proxy", - "targetAddr": "IP / Nom d'hôte", + "targetAddr": "Hôte", "targetPort": "Port", "targetProtocol": "Protocole", "targetTlsSettings": "Configuration sécurisée de connexion", - "targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour votre ressource", + "targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour la ressource", "targetTlsSettingsAdvanced": "Paramètres TLS avancés", "targetTlsSni": "Nom du serveur TLS", "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": "Définir des cibles pour acheminer le trafic vers les services backend", "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", "targetSubmit": "Ajouter une cible", - "targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à votre backend.", + "targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à l'arrière-plan.", "targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.", "targetsSubmit": "Enregistrer les cibles", "addTarget": "Ajouter une cible", @@ -516,9 +528,11 @@ "targetCreatedDescription": "La cible a été créée avec succès", "targetErrorCreate": "Impossible de créer la cible", "targetErrorCreateDescription": "Une erreur s'est produite lors de la création de la cible", + "tlsServerName": "Nom du serveur TLS", + "tlsServerNameDescription": "Le nom du serveur TLS à utiliser pour la SNI", "save": "Enregistrer", "proxyAdditional": "Paramètres de proxy supplémentaires", - "proxyAdditionalDescription": "Configurer la façon dont votre ressource gère les paramètres de proxy", + "proxyAdditionalDescription": "Configurer comment la ressource gère les paramètres du proxy", "proxyCustomHeader": "En-tête Host personnalisé", "proxyCustomHeaderDescription": "L'en-tête host à définir lors du proxy des requêtes. Laissez vide pour utiliser la valeur par défaut.", "proxyAdditionalSubmit": "Enregistrer les paramètres de proxy", @@ -558,7 +572,7 @@ "rulesMatchType": "Type de correspondance", "value": "Valeur", "rulesAbout": "À propos des règles", - "rulesAboutDescription": "Les règles vous permettent de contrôler l'accès à votre ressource en fonction d'un ensemble de critères. Vous pouvez créer des règles pour autoriser ou refuser l'accès basé sur l'adresse IP ou le chemin URL.", + "rulesAboutDescription": "Les règles vous permettent de contrôler l'accès à la ressource en fonction d'un ensemble de critères. Vous pouvez créer des règles pour autoriser ou refuser l'accès en fonction de l'adresse IP ou du chemin d'URL.", "rulesActions": "Actions", "rulesActionAlwaysAllow": "Toujours autoriser : Contourner toutes les méthodes d'authentification", "rulesActionAlwaysDeny": "Toujours refuser : Bloquer toutes les requêtes ; aucune authentification ne peut être tentée", @@ -570,7 +584,7 @@ "rulesEnable": "Activer les règles", "rulesEnableDescription": "Activer ou désactiver l'évaluation des règles pour cette ressource", "rulesResource": "Configuration des règles de ressource", - "rulesResourceDescription": "Configurer les règles pour contrôler l'accès à votre ressource", + "rulesResourceDescription": "Configurer les règles pour contrôler l'accès à la ressource", "ruleSubmit": "Ajouter une règle", "rulesNoOne": "Aucune règle. Ajoutez une règle en utilisant le formulaire.", "rulesOrder": "Les règles sont évaluées par priorité dans l'ordre croissant.", @@ -586,7 +600,7 @@ "none": "Aucun", "unknown": "Inconnu", "resources": "Ressources", - "resourcesDescription": "Les ressources sont des proxys vers des applications exécutées sur votre réseau privé. Créez une ressource pour tout service HTTP/HTTPS ou TCP/UDP brut sur votre réseau privé. Chaque ressource doit être connectée à un site pour permettre une connectivité privée et sécurisée via un tunnel WireGuard chiffré.", + "resourcesDescription": "Les ressources sont des proxy pour les applications exécutées sur le réseau privé. Créez une ressource pour tous les services HTTP/HTTPS ou TCP/UDP bruts sur votre réseau privé. Chaque ressource doit être connectée à un site pour permettre une connectivité privée et sécurisée via un tunnel WireGuard chiffré.", "resourcesWireGuardConnect": "Connectivité sécurisée avec chiffrement WireGuard", "resourcesMultipleAuthenticationMethods": "Configurer plusieurs méthodes d'authentification", "resourcesUsersRolesAccess": "Contrôle d'accès basé sur les utilisateurs et les rôles", @@ -597,7 +611,7 @@ "resourceSelect": "Sélectionner une ressource", "shareLinks": "Liens de partage", "share": "Liens partageables", - "shareDescription2": "Créez des liens partageables vers vos ressources. Les liens fournissent un accès temporaire ou illimité à votre ressource. Vous pouvez configurer la durée d'expiration du lien lors de sa création.", + "shareDescription2": "Créez des liens partageables vers des ressources. Les liens fournissent un accès temporaire ou illimité à votre ressource. Vous pouvez configurer la durée d'expiration du lien lorsque vous en créez un.", "shareEasyCreate": "Facile à créer et à partager", "shareConfigurableExpirationDuration": "Durée d'expiration configurable", "shareSecureAndRevocable": "Sécurisé et révocable", @@ -607,19 +621,19 @@ "unknownCommand": "Commande inconnue", "newtErrorFetchReleases": "Échec de la récupération des informations de version : {err}", "newtErrorFetchLatest": "Erreur lors de la récupération de la dernière version : {err}", - "newtEndpoint": "Point de terminaison Newt", - "newtId": "ID Newt", - "newtSecretKey": "Clé secrète Newt", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Secrète", "architecture": "Architecture", "sites": "Nœuds", - "siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.", + "siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser des ressources internes en utilisant l'adresse IP du pair.", "siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard", "siteWgManualConfigurationRequired": "Configuration manuelle requise", "userErrorNotAdminOrOwner": "L'utilisateur n'est pas un administrateur ou un propriétaire", "pangolinSettings": "Paramètres - Pangolin", "accessRoleYour": "Votre rôle :", - "accessRoleSelect2": "Sélectionner un rôle", - "accessUserSelect": "Sélectionner un utilisateur", + "accessRoleSelect2": "Sélectionner les rôles", + "accessUserSelect": "Sélectionner les utilisateurs", "otpEmailEnter": "Entrer un e-mail", "otpEmailEnterDescription": "Appuyez sur Entrée pour ajouter un e-mail après l'avoir saisi dans le champ.", "otpEmailErrorInvalid": "Adresse e-mail invalide. Le caractère générique (*) doit être la partie locale entière.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Définir le code PIN", "resourcePincodeSetupTitleDescription": "Définir un code PIN pour protéger cette ressource", "resourceRoleDescription": "Les administrateurs peuvent toujours accéder à cette ressource.", - "resourceUsersRoles": "Utilisateurs et rôles", + "resourceUsersRoles": "Contrôles d'accès", "resourceUsersRolesDescription": "Configurer quels utilisateurs et rôles peuvent visiter cette ressource", "resourceUsersRolesSubmit": "Enregistrer les utilisateurs et les rôles", "resourceWhitelistSave": "Enregistré avec succès", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Transférer la ressource", "siteDestination": "Site de destination", "searchSites": "Rechercher des sites", + "countries": "Pays", "accessRoleCreate": "Créer un rôle", "accessRoleCreateDescription": "Créer un nouveau rôle pour regrouper les utilisateurs et gérer leurs permissions.", "accessRoleCreateSubmit": "Créer un rôle", @@ -766,15 +781,15 @@ "idpOidcConfigure": "Configuration OAuth2/OIDC", "idpOidcConfigureDescription": "Configurer les points de terminaison et les identifiants du fournisseur OAuth2/OIDC", "idpClientId": "ID Client", - "idpClientIdDescription": "L'ID client OAuth2 de votre fournisseur d'identité", + "idpClientIdDescription": "L'identifiant client OAuth2 du fournisseur d'identité", "idpClientSecret": "Secret Client", - "idpClientSecretDescription": "Le secret client OAuth2 de votre fournisseur d'identité", + "idpClientSecretDescription": "Le secret du client OAuth2 du fournisseur d'identité", "idpAuthUrl": "URL d'autorisation", "idpAuthUrlDescription": "L'URL du point de terminaison d'autorisation OAuth2", "idpTokenUrl": "URL du jeton", "idpTokenUrlDescription": "L'URL du point de terminaison du jeton OAuth2", "idpOidcConfigureAlert": "Information importante", - "idpOidcConfigureAlertDescription": "Après avoir créé le fournisseur d'identité, vous devrez configurer l'URL de rappel dans les paramètres de votre fournisseur d'identité. L'URL de rappel sera fournie après la création réussie.", + "idpOidcConfigureAlertDescription": "Après avoir créé le fournisseur d'identité, vous devrez configurer l'URL de rappel dans les paramètres du fournisseur d'identité. L'URL de rappel sera fournie après la création réussie.", "idpToken": "Configuration du jeton", "idpTokenDescription": "Configurer comment extraire les informations utilisateur du jeton ID", "idpJmespathAbout": "À propos de JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Créer le fournisseur d'identité", "orgPolicies": "Politiques d'organisation", "idpSettings": "Paramètres de {idpName}", - "idpCreateSettingsDescription": "Configurer les paramètres de votre fournisseur d'identité", + "idpCreateSettingsDescription": "Configurer les paramètres du fournisseur d'identité", "roleMapping": "Mappage des rôles", "orgMapping": "Mappage d'organisation", "orgPoliciesSearch": "Rechercher des politiques d'organisation...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Fournisseur d'identité mis à jour avec succès", "redirectUrl": "URL de redirection", "redirectUrlAbout": "À propos de l'URL de redirection", - "redirectUrlAboutDescription": "C'est l'URL vers laquelle les utilisateurs seront redirigés après l'authentification. Vous devez configurer cette URL dans les paramètres de votre fournisseur d'identité.", + "redirectUrlAboutDescription": "C'est l'URL vers laquelle les utilisateurs seront redirigés après l'authentification. Vous devez configurer cette URL dans les paramètres du fournisseur d'identité.", "pangolinAuth": "Auth - Pangolin", "verificationCodeLengthRequirements": "Votre code de vérification doit comporter 8 caractères.", "errorOccurred": "Une erreur s'est produite", @@ -909,6 +924,10 @@ "passwordResetSent": "Nous allons envoyer un code de réinitialisation à cette adresse e-mail.", "passwordResetCode": "Code de réinitialisation", "passwordResetCodeDescription": "Vérifiez votre e-mail pour le code de réinitialisation.", + "generatePasswordResetCode": "Générer le code de réinitialisation du mot de passe", + "passwordResetCodeGenerated": "Code de réinitialisation du mot de passe généré", + "passwordResetCodeGeneratedDescription": "Partagez ce code avec l'utilisateur. Il peut l'utiliser pour réinitialiser son mot de passe.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nouveau mot de passe", "passwordNewConfirm": "Confirmer le nouveau mot de passe", "changePassword": "Changer le mot de passe", @@ -926,6 +945,9 @@ "pincodeAuth": "Code d'authentification", "pincodeSubmit2": "Soumettre le code", "passwordResetSubmit": "Demander la réinitialisation", + "passwordResetAlreadyHaveCode": "Entrez le code de réinitialisation du mot de passe", + "passwordResetSmtpRequired": "Veuillez contacter votre administrateur", + "passwordResetSmtpRequiredDescription": "Un code de réinitialisation du mot de passe est requis pour réinitialiser votre mot de passe. Veuillez contacter votre administrateur pour obtenir de l'aide.", "passwordBack": "Retour au mot de passe", "loginBack": "Retour à la connexion", "signup": "S'inscrire", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Lister les ressources de site", "actionUpdateSiteResource": "Mettre à jour une ressource de site", "actionListInvitations": "Lister les invitations", + "actionExportLogs": "Exporter les journaux", + "actionViewLogs": "Voir les logs", "noneSelected": "Aucune sélection", "orgNotFound2": "Aucune organisation trouvée.", "searchProgress": "Rechercher...", "create": "Créer", "orgs": "Organisations", "loginError": "Une erreur s'est produite lors de la connexion", + "loginRequiredForDevice": "La connexion est requise pour authentifier votre appareil.", "passwordForgot": "Mot de passe oublié ?", "otpAuth": "Authentification à deux facteurs", "otpAuthDescription": "Entrez le code de votre application d'authentification ou l'un de vos codes de secours à usage unique.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Domicile", "sidebarSites": "Nœuds", "sidebarResources": "Ressource", + "sidebarProxyResources": "Publique", + "sidebarClientResources": "Privé", "sidebarAccessControl": "Contrôle d'accès", + "sidebarLogsAndAnalytics": "Journaux & Analytiques", "sidebarUsers": "Utilisateurs", + "sidebarAdmin": "Administrateur", "sidebarInvitations": "Invitations", "sidebarRoles": "Rôles", - "sidebarShareableLinks": "Liens partagables", + "sidebarShareableLinks": "Liens", "sidebarApiKeys": "Clés API", "sidebarSettings": "Réglages", "sidebarAllUsers": "Tous les utilisateurs", "sidebarIdentityProviders": "Fournisseurs d'identité", "sidebarLicense": "Licence", "sidebarClients": "Clients", + "sidebarUserDevices": "Utilisateurs", + "sidebarMachineClients": "Machines", "sidebarDomains": "Domaines", + "sidebarGeneral": "Généraux", + "sidebarLogAndAnalytics": "Journaux & Analytiques", "sidebarBluePrints": "Configs", + "sidebarOrganization": "Organisation", + "sidebarLogsAnalytics": "Analyses", "blueprints": "Configs", "blueprintsDescription": "Appliquer les configurations déclaratives et afficher les exécutions précédentes", "blueprintAdd": "Ajouter une Config", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Voir le résultat du plan appliqué et les erreurs qui se sont produites", "blueprintInfo": "Informations sur la Config", "message": "Message", - "blueprintContentsDescription": "Définissez le contenu YAML décrivant votre infrastructure", + "blueprintContentsDescription": "Définir le contenu YAML décrivant l'infrastructure", "blueprintErrorCreateDescription": "Une erreur s'est produite lors de l'application de la config", "blueprintErrorCreate": "Erreur lors de la création de la config", "searchBlueprintProgress": "Rechercher des configs...", @@ -1230,15 +1265,15 @@ "loading": "Chargement", "restart": "Redémarrer", "domains": "Domaines", - "domainsDescription": "Gérer les domaines de votre organisation", + "domainsDescription": "Créer et gérer les domaines disponibles dans l'organisation", "domainsSearch": "Rechercher des domaines...", "domainAdd": "Ajouter un domaine", - "domainAddDescription": "Enregistrez un nouveau domaine avec votre organisation", + "domainAddDescription": "Inscrire un nouveau domaine à l'organisation", "domainCreate": "Créer un domaine", "domainCreatedDescription": "Domaine créé avec succès", "domainDeletedDescription": "Domaine supprimé avec succès", - "domainQuestionRemove": "Êtes-vous sûr de vouloir supprimer le domaine de votre compte ?", - "domainMessageRemove": "Une fois supprimé, le domaine ne sera plus associé à votre compte.", + "domainQuestionRemove": "Êtes-vous sûr de vouloir supprimer le domaine ?", + "domainMessageRemove": "Une fois supprimé, le domaine ne sera plus associé à l'organisation.", "domainConfirmDelete": "Confirmer la suppression du domaine", "domainDelete": "Supprimer le domaine", "domain": "Domaine", @@ -1257,7 +1292,7 @@ "pending": "En attente", "sidebarBilling": "Facturation", "billing": "Facturation", - "orgBillingDescription": "Gérez vos informations de facturation et vos abonnements", + "orgBillingDescription": "Gérer les informations de facturation et les abonnements", "github": "GitHub", "pangolinHosted": "Pangolin Hébergement", "fossorial": "Fossorial", @@ -1287,7 +1322,7 @@ "dismissAll": "Tout cacher", "pangolinUpdateAvailable": "Mise à jour disponible", "pangolinUpdateAvailableInfo": "La version {version} est prête à être installée", - "pangolinUpdateAvailableReleaseNotes": "Voir les notes de version", + "pangolinUpdateAvailableReleaseNotes": "Voir les notes de publication", "newtUpdateAvailable": "Mise à jour disponible", "newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.", "domainPickerEnterDomain": "Domaine", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Vérification de la disponibilité...", - "domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé. Essayez un autre domaine ou vérifiez les paramètres de domaine de votre organisation.", + "domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé. Essayez un autre domaine ou vérifiez les paramètres de domaine de l'organisation.", "domainPickerOrganizationDomains": "Domaines de l'organisation", "domainPickerProvidedDomains": "Domaines fournis", "domainPickerSubdomain": "Sous-domaine : {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Modifier l'abonnement", "billingStartSubscription": "Démarrer l'abonnement", "billingRecurringCharge": "Frais récurrents", - "billingManageSubscriptionSettings": "Gérez les paramètres et préférences de votre abonnement", + "billingManageSubscriptionSettings": "Gérer les paramètres et préférences d'abonnement", "billingNoActiveSubscription": "Vous n'avez pas d'abonnement actif. Commencez votre abonnement pour augmenter les limites d'utilisation.", "billingFailedToLoadSubscription": "Échec du chargement de l'abonnement", "billingFailedToLoadUsage": "Échec du chargement de l'utilisation", @@ -1345,9 +1380,9 @@ "billingPortalError": "Erreur du portail", "billingDataUsageInfo": "Vous êtes facturé pour toutes les données transférées via vos tunnels sécurisés lorsque vous êtes connecté au cloud. Cela inclut le trafic entrant et sortant sur tous vos sites. Lorsque vous atteignez votre limite, vos sites se déconnecteront jusqu'à ce que vous mettiez à niveau votre plan ou réduisiez l'utilisation. Les données ne sont pas facturées lors de l'utilisation de nœuds.", "billingOnlineTimeInfo": "Vous êtes facturé en fonction de la durée de connexion de vos sites au cloud. Par exemple, 44 640 minutes équivaut à un site fonctionnant 24/7 pendant un mois complet. Lorsque vous atteignez votre limite, vos sites se déconnecteront jusqu'à ce que vous mettiez à niveau votre forfait ou réduisiez votre consommation. Le temps n'est pas facturé lors de l'utilisation de nœuds.", - "billingUsersInfo": "Vous êtes facturé pour chaque utilisateur dans votre organisation. La facturation est calculée quotidiennement en fonction du nombre de comptes utilisateurs actifs dans votre organisation.", - "billingDomainInfo": "Vous êtes facturé pour chaque domaine dans votre organisation. La facturation est calculée quotidiennement en fonction du nombre de comptes de domaine actifs dans votre organisation.", - "billingRemoteExitNodesInfo": "Vous êtes facturé pour chaque nœud géré dans votre organisation. La facturation est calculée quotidiennement en fonction du nombre de nœuds gérés actifs dans votre organisation.", + "billingUsersInfo": "Vous êtes facturé pour chaque utilisateur de l'organisation. La facturation est calculée quotidiennement en fonction du nombre de comptes d'utilisateurs actifs dans votre organisation.", + "billingDomainInfo": "Vous êtes facturé pour chaque domaine de l'organisation. La facturation est calculée quotidiennement en fonction du nombre de comptes de domaine actifs dans votre organisation.", + "billingRemoteExitNodesInfo": "Vous êtes facturé pour chaque noeud géré dans l'organisation. La facturation est calculée quotidiennement en fonction du nombre de nœuds gérés actifs dans votre organisation.", "domainNotFound": "Domaine introuvable", "domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.", "failed": "Échec", @@ -1430,29 +1465,32 @@ "and": "et", "privacyPolicy": "la politique de confidentialité" }, + "signUpMarketing": { + "keepMeInTheLoop": "Gardez-moi dans la boucle avec des nouvelles, des mises à jour et de nouvelles fonctionnalités par courriel." + }, "siteRequired": "Le site est requis.", "olmTunnel": "Tunnel Olm", "olmTunnelDescription": "Utilisez Olm pour la connectivité client", "errorCreatingClient": "Erreur lors de la création du client", "clientDefaultsNotFound": "Les paramètres par défaut du client sont introuvables", "createClient": "Créer un client", - "createClientDescription": "Créez un nouveau client pour vous connecter à vos sites", + "createClientDescription": "Créer un nouveau client pour accéder aux ressources privées", "seeAllClients": "Voir tous les clients", "clientInformation": "Informations client", "clientNamePlaceholder": "Nom du client", "address": "Adresse", "subnetPlaceholder": "Sous-réseau", - "addressDescription": "L'adresse que ce client utilisera pour la connectivité", + "addressDescription": "L'adresse interne du client. Doit être dans le sous-réseau de l'organisation.", "selectSites": "Sélectionner des sites", "sitesDescription": "Le client aura une connectivité vers les sites sélectionnés", "clientInstallOlm": "Installer Olm", "clientInstallOlmDescription": "Faites fonctionner Olm sur votre système", - "clientOlmCredentials": "Identifiants Olm", - "clientOlmCredentialsDescription": "C'est ainsi qu'Olm s'authentifiera auprès du serveur", - "olmEndpoint": "Point de terminaison Olm", - "olmId": "ID Olm", - "olmSecretKey": "Clé secrète Olm", - "clientCredentialsSave": "Enregistrez vos identifiants", + "clientOlmCredentials": "Identifiants", + "clientOlmCredentialsDescription": "C'est ainsi que le client s'authentifie avec le serveur", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Secrète", + "clientCredentialsSave": "Enregistrer les informations d'identification", "clientCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.", "generalSettingsDescription": "Configurez les paramètres généraux pour ce client", "clientUpdated": "Client mis à jour", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Une erreur s'est produite lors de la récupération des sites.", "olmErrorFetchReleases": "Une erreur s'est produite lors de la récupération des versions d'Olm.", "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 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é", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Surveiller la vie de cette cible. Vous pouvez surveiller un point de terminaison différent de la cible si nécessaire.", "healthScheme": "Méthode", "healthSelectScheme": "Sélectionnez la méthode", + "healthCheckPortInvalid": "Le port du bilan de santé doit être compris entre 1 et 65535", "healthCheckPath": "Chemin d'accès", "healthHostname": "IP / Hôte", "healthPort": "Port", "healthCheckPathDescription": "Le chemin à vérifier pour le statut de santé.", - "healthyIntervalSeconds": "Intervalle sain", - "unhealthyIntervalSeconds": "Intervalle en mauvaise santé", + "healthyIntervalSeconds": "Intervalle de santé (sec)", + "unhealthyIntervalSeconds": "Intervalle malsain (sec)", "IntervalSeconds": "Intervalle sain", - "timeoutSeconds": "Délai", + "timeoutSeconds": "Délai d'attente (sec)", "timeIsInSeconds": "Le temps est exprimé en secondes", "retryAttempts": "Tentatives de réessai", "expectedResponseCodes": "Codes de réponse attendus", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Modifier le domaine", "siteName": "Nom du site", "proxyPort": "Port", - "resourcesTableProxyResources": "Ressources proxy", - "resourcesTableClientResources": "Ressources client", + "resourcesTableProxyResources": "Publique", + "resourcesTableClientResources": "Privé", "resourcesTableNoProxyResourcesFound": "Aucune ressource proxy trouvée.", "resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.", "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "Ces ressources sont à utiliser avec", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Clients", "resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.", "resourcesTableNoTargets": "Aucune cible", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Hors ligne", "resourcesTableUnknown": "Inconnu", "resourcesTableNotMonitored": "Non-monitoré", - "editInternalResourceDialogEditClientResource": "Modifier la ressource client", - "editInternalResourceDialogUpdateResourceProperties": "Mettez à jour les propriétés de la ressource et la configuration de la cible pour {resourceName}.", + "editInternalResourceDialogEditClientResource": "Modifier une ressource privée", + "editInternalResourceDialogUpdateResourceProperties": "Mettre à jour la configuration de la ressource et les contrôles d'accès pour {resourceName}", "editInternalResourceDialogResourceProperties": "Propriétés de la ressource", "editInternalResourceDialogName": "Nom", "editInternalResourceDialogProtocol": "Protocole", @@ -1560,11 +1597,22 @@ "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", + "editInternalResourceDialogPortModeRequired": "Protocole, port proxy et port de destination sont requis pour le mode port", + "editInternalResourceDialogMode": "Mode", + "editInternalResourceDialogModePort": "Port", + "editInternalResourceDialogModeHost": "Hôte", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Destination", + "editInternalResourceDialogDestinationHostDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.", + "editInternalResourceDialogDestinationIPDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.", + "editInternalResourceDialogDestinationCidrDescription": "La gamme CIDR de la ressource sur le réseau du site.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Un alias DNS interne optionnel pour cette ressource.", "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é.", + "createInternalResourceDialogCreateClientResource": "Créer une ressource privée", + "createInternalResourceDialogCreateClientResourceDescription": "Créer une nouvelle ressource qui ne sera accessible qu'aux clients connectés à l'organisation", "createInternalResourceDialogResourceProperties": "Propriétés de la ressource", "createInternalResourceDialogName": "Nom", "createInternalResourceDialogSite": "Site", @@ -1593,11 +1641,22 @@ "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", + "createInternalResourceDialogPortModeRequired": "Protocole, port proxy et port de destination sont requis pour le mode port", + "createInternalResourceDialogMode": "Mode", + "createInternalResourceDialogModePort": "Port", + "createInternalResourceDialogModeHost": "Hôte", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Destination", + "createInternalResourceDialogDestinationHostDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.", + "createInternalResourceDialogDestinationCidrDescription": "La gamme CIDR de la ressource sur le réseau du site.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Un alias DNS interne optionnel pour cette ressource.", "siteConfiguration": "Configuration", "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.", + "siteAcceptClientConnectionsDescription": "Autoriser les utilisateurs et les clients à accéder aux ressources de ce site. Cela peut être modifié plus tard.", + "siteAddress": "Adresse du site (Avancé)", + "siteAddressDescription": "L'adresse interne du site. Doit être dans le sous-réseau de l'organisation.", + "siteNameDescription": "Le nom d'affichage du site qui peut être modifié plus tard.", "autoLoginExternalIdp": "Connexion automatique avec IDP externe", "autoLoginExternalIdpDescription": "Rediriger immédiatement l'utilisateur vers l'IDP externe pour l'authentification.", "selectIdp": "Sélectionner l'IDP", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.", "autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.", "remoteExitNodeManageRemoteExitNodes": "Nœuds distants", - "remoteExitNodeDescription": "Héberger un ou plusieurs nœuds distants pour étendre votre connectivité réseau et réduire la dépendance sur le cloud", + "remoteExitNodeDescription": "Héberger un ou plusieurs nœuds distants pour étendre la connectivité réseau et réduire la dépendance sur le cloud", "remoteExitNodes": "Nœuds", "searchRemoteExitNodes": "Rechercher des nœuds...", "remoteExitNodeAdd": "Ajouter un noeud", @@ -1623,11 +1682,11 @@ "sidebarRemoteExitNodes": "Nœuds distants", "remoteExitNodeCreate": { "title": "Créer un noeud", - "description": "Créer un nouveau nœud pour étendre votre connectivité réseau", + "description": "Créer un nouveau nœud pour étendre la connectivité réseau", "viewAllButton": "Voir tous les nœuds", "strategy": { "title": "Stratégie de création", - "description": "Choisissez ceci pour configurer manuellement votre nœud ou générer de nouveaux identifiants.", + "description": "Choisissez ceci pour configurer manuellement le noeud ou générer de nouveaux identifiants.", "adopt": { "title": "Adopter un nœud", "description": "Choisissez ceci si vous avez déjà les identifiants pour le noeud." @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Informations d'identification générées", - "description": "Utilisez ces identifiants générés pour configurer votre noeud", + "description": "Utilisez ces identifiants générés pour configurer le noeud", "nodeIdTitle": "Nœud ID", "secretTitle": "Secret", "saveCredentialsTitle": "Ajouter des identifiants à la config", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Type de fournisseur d'identité", "roleMappingExpressionPlaceholder": "ex: contenu(groupes) && 'admin' || 'membre'", "idpGoogleConfiguration": "Configuration Google", - "idpGoogleConfigurationDescription": "Configurer vos identifiants Google OAuth2", - "idpGoogleClientIdDescription": "Votre identifiant client Google OAuth2", - "idpGoogleClientSecretDescription": "Votre secret client Google OAuth2", + "idpGoogleConfigurationDescription": "Configurer les identifiants Google OAuth2", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Secret client OAuth2 de Google", "idpAzureConfiguration": "Configuration de l'entra ID Azure", - "idpAzureConfigurationDescription": "Configurer vos identifiants OAuth2 Azure Entra", + "idpAzureConfigurationDescription": "Configurer les identifiants OAuth2 Azure Entra ID", "idpTenantId": "ID du locataire", - "idpTenantIdPlaceholder": "votre-locataire-id", - "idpAzureTenantIdDescription": "Votre ID de locataire Azure (trouvé dans l'aperçu Azure Active Directory)", - "idpAzureClientIdDescription": "Votre ID client d'enregistrement de l'application Azure", - "idpAzureClientSecretDescription": "Le secret de votre client d'enregistrement Azure App", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "ID du locataire Azure (trouvé dans l'aperçu Azure Active Directory)", + "idpAzureClientIdDescription": "ID client d'enregistrement de l'application Azure", + "idpAzureClientSecretDescription": "Secret du client d'enregistrement de l'application Azure", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Configuration Google", "idpAzureConfigurationTitle": "Configuration de l'entra ID Azure", "idpTenantIdLabel": "ID du locataire", - "idpAzureClientIdDescription2": "Votre ID client d'enregistrement de l'application Azure", - "idpAzureClientSecretDescription2": "Le secret de votre client d'enregistrement Azure App", + "idpAzureClientIdDescription2": "ID client d'enregistrement de l'application Azure", + "idpAzureClientSecretDescription2": "Secret du client d'enregistrement de l'application Azure", "idpGoogleDescription": "Fournisseur Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Sous-réseau", "subnetDescription": "Le sous-réseau de la configuration réseau de cette organisation.", "authPage": "Page d'authentification", - "authPageDescription": "Configurer la page d'authentification de votre organisation", + "authPageDescription": "Configurer la page d'authentification de l'organisation", "authPageDomain": "Domaine de la page d'authentification", "noDomainSet": "Aucun domaine défini", "changeDomain": "Changer de domaine", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Définir le domaine de la page d'authentification", "failedToFetchCertificate": "Impossible de récupérer le certificat", "failedToRestartCertificate": "Échec du redémarrage du certificat", - "addDomainToEnableCustomAuthPages": "Ajouter un domaine pour activer les pages d'authentification personnalisées pour votre organisation", + "addDomainToEnableCustomAuthPages": "Ajouter un domaine pour activer les pages d'authentification personnalisées pour l'organisation", "selectDomainForOrgAuthPage": "Sélectionnez un domaine pour la page d'authentification de l'organisation", "domainPickerProvidedDomain": "Domaine fourni", "domainPickerFreeProvidedDomain": "Domaine fourni gratuitement", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "La «{sub}» n'a pas pu être validée pour {domain}.", "domainPickerSubdomainSanitized": "Sous-domaine nettoyé", "domainPickerSubdomainCorrected": "\"{sub}\" a été corrigé à \"{sanitized}\"", - "orgAuthSignInTitle": "Connectez-vous à votre organisation", + "orgAuthSignInTitle": "Se connecter à l'organisation", "orgAuthChooseIdpDescription": "Choisissez votre fournisseur d'identité pour continuer", "orgAuthNoIdpConfigured": "Cette organisation n'a aucun fournisseur d'identité configuré. Vous pouvez vous connecter avec votre identité Pangolin à la place.", "orgAuthSignInWithPangolin": "Se connecter avec Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Activer l'authentification à deux facteurs", "completeSecuritySteps": "Compléter les étapes de sécurité", "securitySettings": "Paramètres de sécurité", - "securitySettingsDescription": "Configurer les politiques de sécurité de votre organisation", + "securitySettingsDescription": "Configurer les politiques de sécurité de l'organisation", "requireTwoFactorForAllUsers": "Exiger une authentification à deux facteurs pour tous les utilisateurs", "requireTwoFactorDescription": "Lorsque cette option est activée, tous les utilisateurs internes de cette organisation doivent avoir l'authentification à deux facteurs pour accéder à l'organisation.", "requireTwoFactorDisabledDescription": "Cette fonctionnalité nécessite une licence valide (Entreprise) ou un abonnement actif (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Édition Entreprise", "unlicensed": "Sans licence", "beta": "Bêta", - "manageClients": "Gérer les clients", - "manageClientsDescription": "Les clients sont des appareils qui peuvent se connecter à vos sites", + "manageUserDevices": "Périphériques utilisateur", + "manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources", + "manageMachineClients": "Gérer les clients de la machine", + "manageMachineClientsDescription": "Créer et gérer des clients que les serveurs et les systèmes utilisent pour se connecter en privé aux ressources", + "clientsTableUserClients": "Utilisateur", + "clientsTableMachineClients": "Machine", "licenseTableValidUntil": "Valable jusqu'au", "saasLicenseKeysSettingsTitle": "Licences Entreprise", "saasLicenseKeysSettingsDescription": "Générer et gérer les clés de licence Entreprise pour les instances Pangolin auto-hébergées", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "bande", "sidebarEnableEnterpriseLicense": "Activer la licence Entreprise", "cannotbeUndone": "Cela ne peut pas être annulé.", - "toConfirm": "pour confirmer", + "toConfirm": "pour confirmer.", "deleteClientQuestion": "Êtes-vous sûr de vouloir supprimer le client du site et de l'organisation ?", "clientMessageRemove": "Une fois supprimé, le client ne pourra plus se connecter au site.", "sidebarLogs": "Journaux", "request": "Demander", + "requests": "Requêtes", "logs": "Journaux", "logsSettingsDescription": "Surveiller les logs collectés à partir de cette organisation", "searchLogs": "Rechercher dans les journaux...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Raison", "requestLogs": "Journal des requêtes", + "requestAnalytics": "Demander des analyses", "host": "Hôte", "location": "Localisation", "actionLogs": "Journaux des actions", @@ -2029,6 +2094,7 @@ "logRetention": "Journaliser la rétention", "logRetentionDescription": "Gérer la durée de conservation des différents types de logs pour cette organisation ou les désactiver", "requestLogsDescription": "Voir les journaux détaillés des requêtes pour les ressources de cette organisation", + "requestAnalyticsDescription": "Voir les analyses détaillées des demandes pour les ressources de cette organisation", "logRetentionRequestLabel": "Demander la rétention des journaux", "logRetentionRequestDescription": "Durée de conservation des journaux de requêtes", "logRetentionAccessLabel": "Rétention du journal d'accès", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 jours", "logRetention90Days": "90 jours", "logRetentionForever": "Pour toujours", + "logRetentionEndOfFollowingYear": "Fin de l'année suivante", "actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation", "accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation", "licenseRequiredToUse": "Une licence Entreprise est nécessaire pour utiliser cette fonctionnalité.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Préférez le certificat Wildcard", "unverified": "Non vérifié", "domainSetting": "Paramètres de domaine", - "domainSettingDescription": "Configurer les paramètres de votre domaine", + "domainSettingDescription": "Configurer les paramètres du domaine", "preferWildcardCertDescription": "Tentative de génération d'un certificat générique (nécessite un résolveur de certificat correctement configuré).", "recordName": "Nom de l'enregistrement", "auto": "Automatique", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Une version mise à jour de Olm est disponible. Veuillez mettre à jour vers la dernière version pour la meilleure expérience.", "client": "Client", "proxyProtocol": "Paramètres du protocole proxy", - "proxyProtocolDescription": "Configurer le protocole Proxy pour préserver les adresses IP du client pour les services TCP/UDP.", + "proxyProtocolDescription": "Configurer le protocole Proxy pour préserver les adresses IP du client pour les services TCP.", "enableProxyProtocol": "Activer le protocole Proxy", - "proxyProtocolInfo": "Conserver les adresses IP du client pour les backends TCP/UDP", + "proxyProtocolInfo": "Conserver les adresses IP du client pour les backends TCP", "proxyProtocolVersion": "Version du protocole proxy", "version1": " Version 1 (Recommandé)", "version2": "Version 2", "versionDescription": "La version 1 est basée sur du texte et est largement supportée. La version 2 est binaire et plus efficace mais moins compatible.", "warning": "Avertissement", - "proxyProtocolWarning": "Votre application backend doit être configurée pour accepter les connexions Proxy Protocol. Si votre backend ne prend pas en charge le protocole Proxy, activer ceci va casser toutes les connexions. Assurez-vous de configurer votre backend pour faire confiance aux en-têtes du protocole Proxy de Traefik.", + "proxyProtocolWarning": "L'application backend doit être configurée pour accepter les connexions Proxy Protocol. Si votre backend ne prend pas en charge le protocole Proxy, l'activation de cette option va perturber toutes les connexions, donc n'activez cette option que si vous savez ce que vous faites. Assurez-vous de configurer votre backend pour faire confiance aux en-têtes du protocole Proxy de Traefik.", "restarting": "Redémarrage...", "manual": "Manuel", "messageSupport": "Soutien aux messages", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Message envoyé !", "supportWillContact": "Nous vous contacterons sous peu!", "selectLogRetention": "Sélectionner la durée de rétention des logs", + "terms": "Conditions générales de vente", + "privacy": "Confidentialité", + "security": "Sécurité", + "docs": "Documents", + "deviceActivation": "Activation de l'appareil", + "deviceCodeInvalidFormat": "Le code doit contenir 9 caractères (par exemple, A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Code invalide ou expiré", + "deviceCodeVerifyFailed": "Impossible de vérifier le code de l'appareil", + "signedInAs": "Connecté en tant que", + "deviceCodeEnterPrompt": "Entrez le code affiché sur l'appareil", + "continue": "Continuer", + "deviceUnknownLocation": "Lieu inconnu", + "deviceAuthorizationRequested": "Cette autorisation a été demandée à {location} sur {date}. Assurez-vous que vous faites confiance à cet appareil car il aura accès au compte.", + "deviceLabel": "Appareil : {deviceName}", + "deviceWantsAccess": "veut accéder à votre compte", + "deviceExistingAccess": "Accès existant:", + "deviceFullAccess": "Accès complet à votre compte", + "deviceOrganizationsAccess": "Accès à toutes les organisations auxquelles votre compte a accès", + "deviceAuthorize": "Autoriser {applicationName}", + "deviceConnected": "Appareil connecté !", + "deviceAuthorizedMessage": "L'appareil est autorisé à accéder à votre compte.", + "pangolinCloud": "Nuage de Pangolin", + "viewDevices": "Voir les appareils", + "viewDevicesDescription": "Gérer vos appareils connectés", + "noDevices": "Aucun appareil trouvé", + "dateCreated": "Date de création", + "unnamedDevice": "Appareil sans nom", + "deviceQuestionRemove": "Êtes-vous sûr de vouloir supprimer cet appareil ?", + "deviceMessageRemove": "Cette action ne peut être annulée.", + "deviceDeleteConfirm": "Supprimer l'appareil", + "deleteDevice": "Supprimer l'appareil", + "errorLoadingDevices": "Erreur lors du chargement des appareils", + "failedToLoadDevices": "Impossible de charger les appareils", + "deviceDeleted": "Appareil supprimé", + "deviceDeletedDescription": "L'appareil a été supprimé avec succès.", + "errorDeletingDevice": "Erreur lors de la suppression de l'appareil", + "failedToDeleteDevice": "Échec de la suppression de l'appareil", "showColumns": "Afficher les colonnes", "hideColumns": "Cacher les colonnes", "columnVisibility": "Visibilité des colonnes", @@ -2111,10 +2215,14 @@ "enableSelected": "Activer la sélection", "disableSelected": "Désactiver la sélection", "checkSelectedStatus": "Vérifier le statut de la sélection", + "clients": "Clients", + "accessClientSelect": "Sélectionnez les clients de machine", + "resourceClientDescription": "Les clients qui peuvent accéder à cette ressource", + "regenerate": "Régénérer", "credentials": "Identifiants", "savecredentials": "Enregistrer les identifiants", - "regeneratecredentials": "Re-claver", - "regenerateCredentials": "Régénérer et enregistrer les identifiants", + "regenerateCredentialsButton": "Régénérer les identifiants", + "regenerateCredentials": "Régénérer les identifiants", "generatedcredentials": "Identifiants générés", "copyandsavethesecredentials": "Copier et enregistrer ces identifiants", "copyandsavethesecredentialsdescription": "Ces identifiants ne seront pas affichés à nouveaux une fois cette page fermée. Enregistrez-les maintenant.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Les identifiants ont été régénérés et enregistrés avec succès.", "credentialsSaveError": "Erreur lors de l'enregistrement des identifiants", "credentialsSaveErrorDescription": "Une erreur s'est produite lors de la régénération et l'enregistrement des identifiants.", - "regenerateCredentialsWarning": "La régénération de ces identifiants invalidera ceux actuellement utilisés. Assurez-vous de mettre à jour toutes les configurations qui les utilisent.", + "regenerateCredentialsWarning": "La régénération des identifiants invalidera les identifiants précédents et provoquera une déconnexion. Assurez-vous de mettre à jour toutes les configurations qui utilisent ces identifiants.", "confirm": "Confirmer", "regenerateCredentialsConfirmation": "Voulez-vous vraiment régénérer les identifiants ?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Clé privée", - "featureDisabledTooltip": "Cette fonctionnalité n'est disponible que dans la version entreprise et nécessite une licence pour être utilisée.", "niceId": "Joli ID", "niceIdUpdated": "Joli ID mis à jour", "niceIdUpdatedSuccessfully": "Joli ID mis à jour avec succès", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Erreur lors de la mise à jour du joli ID.", "niceIdCannotBeEmpty": "Merci de renseigner un joli ID", "enterIdentifier": "Entrez l'identifiant", - "identifier": "Identifiant" + "identifier": "Identifiant", + "deviceLoginUseDifferentAccount": "Pas vous ? Utilisez un autre compte.", + "deviceLoginDeviceRequestingAccessToAccount": "Un appareil demande l'accès à ce compte.", + "noData": "Aucune donnée", + "machineClients": "Clients Machines", + "install": "Installer", + "run": "Exécuter", + "clientNameDescription": "Le nom d'affichage du client qui peut être modifié plus tard.", + "clientAddress": "Adresse du client (Avancé)", + "setupFailedToFetchSubnet": "Impossible de récupérer le sous-réseau par défaut", + "setupSubnetAdvanced": "Sous-réseau (Avancé)", + "setupSubnetDescription": "Le sous-réseau du réseau interne de cette organisation.", + "siteRegenerateAndDisconnect": "Régénérer et déconnecter", + "siteRegenerateAndDisconnectConfirmation": "Êtes-vous sûr de vouloir régénérer les identifiants et déconnecter ce site ?", + "siteRegenerateAndDisconnectWarning": "Cela va régénérer les identifiants et déconnecter immédiatement le site. Le site devra être redémarré avec les nouveaux identifiants.", + "siteRegenerateCredentialsConfirmation": "Êtes-vous sûr de vouloir régénérer les identifiants pour ce site ?", + "siteRegenerateCredentialsWarning": "Cela va régénérer les identifiants. Le site restera connecté jusqu'à ce que vous le redémarriez manuellement et utilisez les nouveaux identifiants.", + "clientRegenerateAndDisconnect": "Régénérer et déconnecter", + "clientRegenerateAndDisconnectConfirmation": "Êtes-vous sûr de vouloir régénérer les identifiants et déconnecter ce client?", + "clientRegenerateAndDisconnectWarning": "Cela va régénérer les identifiants et déconnecter immédiatement le client. Le client devra être redémarré avec les nouveaux identifiants.", + "clientRegenerateCredentialsConfirmation": "Êtes-vous sûr de vouloir régénérer les identifiants pour ce client ?", + "clientRegenerateCredentialsWarning": "Cela va régénérer les identifiants. Le client restera connecté jusqu'à ce que vous le redémarriez manuellement et utilisiez les nouveaux identifiants.", + "remoteExitNodeRegenerateAndDisconnect": "Régénérer et déconnecter", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Êtes-vous sûr de vouloir régénérer les identifiants et déconnecter ce noeud de sortie distant ?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Cela va régénérer les identifiants et déconnecter immédiatement le noeud de sortie distant. Le noeud de sortie distant devra être redémarré avec les nouveaux identifiants.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Êtes-vous sûr de vouloir régénérer les informations d'identification pour ce noeud de sortie distant ?", + "remoteExitNodeRegenerateCredentialsWarning": "Cela va régénérer les identifiants. Le noeud de sortie distant restera connecté jusqu'à ce que vous le redémarriez manuellement et utilisez les nouveaux identifiants.", + "agent": "Agent" } diff --git a/messages/it-IT.json b/messages/it-IT.json index 08f1e405..441c9676 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1,12 +1,12 @@ { - "setupCreate": "Crea la tua organizzazione, sito e risorse", + "setupCreate": "Creare l'organizzazione, il sito e le risorse", "setupNewOrg": "Nuova Organizzazione", "setupCreateOrg": "Crea Organizzazione", "setupCreateResources": "Crea Risorse", "setupOrgName": "Nome Dell'Organizzazione", - "orgDisplayName": "Questo è il nome visualizzato della tua organizzazione.", + "orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.", "orgId": "Id Organizzazione", - "setupIdentifierMessage": "Questo è l' identificatore univoco della tua organizzazione. Questo è separato dal nome del display.", + "setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.", "setupErrorIdentifier": "L'ID dell'organizzazione è già utilizzato. Si prega di sceglierne uno diverso.", "componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.", "componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli obiettivi associati al sito verranno rimossi.", "siteQuestionRemove": "Sei sicuro di voler rimuovere il sito dall'organizzazione?", "siteManageSites": "Gestisci Siti", - "siteDescription": "Consenti la connettività alla rete attraverso tunnel sicuri", + "siteDescription": "Creare e gestire siti per abilitare la connettività a reti private", "siteCreate": "Crea Sito", "siteCreateDescription2": "Segui i passaggi qui sotto per creare e collegare un nuovo sito", - "siteCreateDescription": "Crea un nuovo sito per iniziare a connettere le tue risorse", + "siteCreateDescription": "Crea un nuovo sito per iniziare a connettere le risorse", "close": "Chiudi", "siteErrorCreate": "Errore nella creazione del sito", "siteErrorCreateKeyPair": "Coppia di chiavi o valori predefiniti del sito non trovati", @@ -74,7 +74,7 @@ "siteInstallNewt": "Installa Newt", "siteInstallNewtDescription": "Esegui Newt sul tuo sistema", "WgConfiguration": "Configurazione WireGuard", - "WgConfigurationDescription": "Usa la seguente configurazione per connetterti alla tua rete", + "WgConfigurationDescription": "Utilizzare la seguente configurazione per connettersi alla rete", "operatingSystem": "Sistema Operativo", "commands": "Comandi", "recommended": "Consigliato", @@ -87,25 +87,25 @@ "siteUpdated": "Sito aggiornato", "siteUpdatedDescription": "Il sito è stato aggiornato.", "siteGeneralDescription": "Configura le impostazioni generali per questo sito", - "siteSettingDescription": "Configura le impostazioni sul tuo sito", + "siteSettingDescription": "Configura le impostazioni del sito", "siteSetting": "Impostazioni {siteName}", - "siteNewtTunnel": "Tunnel Newt (Consigliato)", - "siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint nella rete. Nessuna configurazione aggiuntiva.", + "siteNewtTunnel": "Nuovo Sito (Consigliato)", + "siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.", "siteWg": "WireGuard Base", "siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", "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": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.", "siteSeeAll": "Vedi Tutti I Siti", - "siteTunnelDescription": "Determina come vuoi connetterti al tuo sito", - "siteNewtCredentials": "Credenziali Newt", - "siteNewtCredentialsDescription": "Questo è come Newt si autenticerà con il server", - "siteCredentialsSave": "Salva Le Tue Credenziali", + "siteTunnelDescription": "Determinare come si desidera connettersi al sito", + "siteNewtCredentials": "Credenziali", + "siteNewtCredentialsDescription": "Questo è come il sito si autenticerà con il server", + "siteCredentialsSave": "Salva le credenziali", "siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.", "siteInfo": "Informazioni Sito", "status": "Stato", "shareTitle": "Gestisci Collegamenti Di Condivisione", - "shareDescription": "Crea link condivisibili per concedere un accesso temporaneo o permanente alle tue risorse", + "shareDescription": "Crea link condivisibili per concedere accesso temporaneo o permanente alle risorse proxy", "shareSearch": "Cerca link condivisi...", "shareCreate": "Crea Link Di Condivisione", "shareErrorDelete": "Impossibile eliminare il link", @@ -121,7 +121,7 @@ "importantNote": "Nota Importante", "shareImportantDescription": "Per motivi di sicurezza, si consiglia di utilizzare le intestazioni su parametri di query quando possibile, in quanto i parametri di query possono essere registrati in log server o cronologia browser.", "token": "Token", - "shareTokenSecurety": "Mantieni sicuro il tuo token di accesso. Non condividerlo in aree accessibili al pubblico o codice lato client.", + "shareTokenSecurety": "Mantenere sicuro il token di accesso. Non condividerlo in aree accessibili al pubblico o codice lato client.", "shareErrorFetchResource": "Recupero delle risorse non riuscito", "shareErrorFetchResourceDescription": "Si è verificato un errore durante il recupero delle risorse", "shareErrorCreate": "Impossibile creare il link di condivisione", @@ -144,8 +144,10 @@ "expires": "Scade", "never": "Mai", "shareErrorSelectResource": "Seleziona una risorsa", - "resourceTitle": "Gestisci Risorse", - "resourceDescription": "Crea proxy sicuri per le tue applicazioni private", + "proxyResourceTitle": "Gestisci Risorse Pubbliche", + "proxyResourceDescription": "Creare e gestire risorse accessibili al pubblico tramite un browser web", + "clientResourceTitle": "Gestisci Risorse Private", + "clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso", "resourcesSearch": "Cerca risorse...", "resourceAdd": "Aggiungi Risorsa", "resourceErrorDelte": "Errore nell'eliminare la risorsa", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Una volta rimossa, la risorsa non sarà più accessibile. Tutti gli obiettivi associati alla risorsa saranno rimossi.", "resourceQuestionRemove": "Sei sicuro di voler rimuovere la risorsa dall'organizzazione?", "resourceHTTP": "Risorsa HTTPS", - "resourceHTTPDescription": "Richieste proxy alla tua app tramite HTTPS utilizzando un sottodominio o un dominio di base.", + "resourceHTTPDescription": "Richieste proxy per l'applicazione tramite HTTPS utilizzando un sottodominio o un dominio base.", "resourceRaw": "Risorsa Raw TCP/UDP", - "resourceRawDescription": "Richieste proxy alla tua app tramite TCP/UDP utilizzando un numero di porta.", + "resourceRawDescription": "Le richieste proxy all'app tramite TCP/UDP utilizzando un numero di porta. Funziona solo quando i siti sono connessi ai nodi.", "resourceCreate": "Crea Risorsa", "resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa", "resourceSeeAll": "Vedi Tutte Le Risorse", @@ -171,22 +173,22 @@ "noCountryFound": "Nessun paese trovato.", "siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.", "resourceType": "Tipo Di Risorsa", - "resourceTypeDescription": "Determina come vuoi accedere alla tua risorsa", + "resourceTypeDescription": "Determinare come accedere alla risorsa", "resourceHTTPSSettings": "Impostazioni HTTPS", - "resourceHTTPSSettingsDescription": "Configura come sarà possibile accedere alla tua risorsa su HTTPS", + "resourceHTTPSSettingsDescription": "Configura come sarà possibile accedere alla risorsa su HTTPS", "domainType": "Tipo Di Dominio", "subdomain": "Sottodominio", "baseDomain": "Dominio Base", - "subdomnainDescription": "Il sottodominio in cui la tua risorsa sarà accessibile.", + "subdomnainDescription": "Il sottodominio in cui la risorsa sarà accessibile.", "resourceRawSettings": "Impostazioni TCP/UDP", - "resourceRawSettingsDescription": "Configura come sarà possibile accedere alla tua risorsa tramite TCP/UDP. Mappare la risorsa a una porta sul server host Pangolin, in modo da poter accedere alla risorsa dal server-public ip:mapped-port.", + "resourceRawSettingsDescription": "Configura come accedere alla risorsa tramite TCP/UDP", "protocol": "Protocollo", "protocolSelect": "Seleziona un protocollo", "resourcePortNumber": "Numero Porta", "resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.", "cancel": "Annulla", "resourceConfig": "Snippet Di Configurazione", - "resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la tua risorsa TCP/UDP", + "resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP", "resourceAddEntrypoints": "Traefik: Aggiungi Ingresso", "resourceExposePorts": "Gerbil: espone le porte in Docker componi", "resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP", @@ -202,14 +204,14 @@ "proxy": "Proxy", "internal": "Interno", "rules": "Regole", - "resourceSettingDescription": "Configura le impostazioni sulla tua risorsa", + "resourceSettingDescription": "Configura le impostazioni sulla risorsa", "resourceSetting": "Impostazioni {resourceName}", - "alwaysAllow": "Consenti Sempre", - "alwaysDeny": "Nega Sempre", + "alwaysAllow": "Autenticazione Bypass", + "alwaysDeny": "Blocca Accesso", "passToAuth": "Passa all'autenticazione", - "orgSettingsDescription": "Configura le impostazioni generali della tua organizzazione", + "orgSettingsDescription": "Configura le impostazioni dell'organizzazione", "orgGeneralSettings": "Impostazioni Organizzazione", - "orgGeneralSettingsDescription": "Gestisci i dettagli dell'organizzazione e la configurazione", + "orgGeneralSettingsDescription": "Gestisci i dettagli e la configurazione dell'organizzazione", "saveGeneralSettings": "Salva Impostazioni Generali", "saveSettings": "Salva Impostazioni", "orgDangerZone": "Zona Pericolosa", @@ -232,7 +234,7 @@ "orgMissing": "ID Organizzazione Mancante", "orgMissingMessage": "Impossibile rigenerare l'invito senza un ID organizzazione.", "accessUsersManage": "Gestisci Utenti", - "accessUsersDescription": "Invita gli utenti e aggiungili ai ruoli per gestire l'accesso alla tua organizzazione", + "accessUsersDescription": "Invita e gestisci gli utenti con accesso a questa organizzazione", "accessUsersSearch": "Cerca utenti...", "accessUserCreate": "Crea Utente", "accessUserRemove": "Rimuovi Utente", @@ -241,13 +243,13 @@ "role": "Ruolo", "nameRequired": "Il nome è obbligatorio", "accessRolesManage": "Gestisci Ruoli", - "accessRolesDescription": "Configura i ruoli per gestire l'accesso alla tua organizzazione", + "accessRolesDescription": "Creare e gestire ruoli per gli utenti nell'organizzazione", "accessRolesSearch": "Ricerca ruoli...", "accessRolesAdd": "Aggiungi Ruolo", "accessRoleDelete": "Elimina Ruolo", "description": "Descrizione", "inviteTitle": "Inviti Aperti", - "inviteDescription": "Gestisci i tuoi inviti ad altri utenti", + "inviteDescription": "Gestisci gli inviti per gli altri utenti a unirsi all'organizzazione", "inviteSearch": "Cerca inviti...", "minutes": "Minuti", "hours": "Ore", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Errore nella creazione della chiave API", "apiKeysErrorSetPermission": "Errore nell'impostazione dei permessi", "apiKeysCreate": "Genera Chiave API", - "apiKeysCreateDescription": "Genera una nuova chiave API per la tua organizzazione", + "apiKeysCreateDescription": "Genera una nuova chiave API per l'organizzazione", "apiKeysGeneralSettings": "Permessi", "apiKeysGeneralSettingsDescription": "Determina cosa può fare questa chiave API", - "apiKeysList": "La Tua Chiave API", - "apiKeysSave": "Salva La Tua Chiave API", + "apiKeysList": "Nuova Chiave Api", + "apiKeysSave": "Salva la chiave API", "apiKeysSaveDescription": "Potrai vederla solo una volta. Assicurati di copiarla in un luogo sicuro.", - "apiKeysInfo": "La tua chiave API è:", + "apiKeysInfo": "La chiave API è:", "apiKeysConfirmCopy": "Ho copiato la chiave API", "generate": "Genera", "done": "Fatto", @@ -424,7 +426,7 @@ "userCreated": "Utente creato", "userCreatedDescription": "L'utente è stato creato con successo.", "userTypeInternal": "Utente Interno", - "userTypeInternalDescription": "Invita un utente a unirsi direttamente alla tua organizzazione.", + "userTypeInternalDescription": "Invita un utente a unirsi direttamente all'organizzazione.", "userTypeExternal": "Utente Esterno", "userTypeExternalDescription": "Crea un utente con un provider di identità esterno.", "accessUserCreateDescription": "Segui i passaggi seguenti per creare un nuovo utente", @@ -436,6 +438,16 @@ "inviteEmailSent": "Invia email di invito all'utente", "inviteValid": "Valido Per", "selectDuration": "Seleziona durata", + "selectResource": "Seleziona Risorsa", + "filterByResource": "Filtra Per Risorsa", + "resetFilters": "Ripristina Filtri", + "totalBlocked": "Richieste Bloccate Da Pangolino", + "totalRequests": "Totale Richieste", + "requestsByCountry": "Richieste Per Paese", + "requestsByDay": "Richieste Per Giorno", + "blocked": "Bloccato", + "allowed": "Consentito", + "topCountries": "Paesi Principali", "accessRoleSelect": "Seleziona ruolo", "inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. Devono accedere al link per accettare l'invito.", "inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Salva Controlli di Accesso", "roles": "Ruoli", "accessUsersRoles": "Gestisci Utenti e Ruoli", - "accessUsersRolesDescription": "Invita utenti e aggiungili ai ruoli per gestire l'accesso alla tua organizzazione", + "accessUsersRolesDescription": "Invita gli utenti e aggiungili ai ruoli per gestire l'accesso all'organizzazione", "key": "Chiave", "createdAt": "Creato Il", "proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.", "proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.", "proxyEnableSSL": "Abilita SSL", - "proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure ai tuoi obiettivi.", + "proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure agli obiettivi.", "target": "Target", "configureTarget": "Configura Obiettivi", "targetErrorFetch": "Impossibile recuperare i target", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Impossibile aggiornare i target", "targetsErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento dei target", "targetTlsUpdate": "Impostazioni TLS aggiornate", - "targetTlsUpdateDescription": "Le tue impostazioni TLS sono state aggiornate con successo", + "targetTlsUpdateDescription": "Le impostazioni TLS sono state aggiornate correttamente", "targetErrorTlsUpdate": "Impossibile aggiornare le impostazioni TLS", "targetErrorTlsUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni TLS", "proxyUpdated": "Impostazioni proxy aggiornate", - "proxyUpdatedDescription": "Le tue impostazioni proxy sono state aggiornate con successo", + "proxyUpdatedDescription": "Le impostazioni del proxy sono state aggiornate con successo", "proxyErrorUpdate": "Impossibile aggiornare le impostazioni proxy", "proxyErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni proxy", - "targetAddr": "IP / Nome host", + "targetAddr": "Host", "targetPort": "Porta", "targetProtocol": "Protocollo", "targetTlsSettings": "Configurazione Connessione Sicura", - "targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa", + "targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la risorsa", "targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate", "targetTlsSni": "Nome Server Tls", "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 backend", + "targetsDescription": "Impostare obiettivi per instradare il traffico verso i servizi di backend", "targetStickySessions": "Abilita Sessioni Persistenti", "targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.", "methodSelect": "Seleziona metodo", "targetSubmit": "Aggiungi Target", - "targetNoOne": "Questa risorsa non ha bersagli. Aggiungi un obiettivo per configurare dove inviare le richieste al tuo backend.", + "targetNoOne": "Questa risorsa non ha destinazioni. Aggiungi un obiettivo per configurare dove inviare richieste al backend.", "targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.", "targetsSubmit": "Salva Target", "addTarget": "Aggiungi Target", @@ -516,9 +528,11 @@ "targetCreatedDescription": "L'obiettivo è stato creato con successo", "targetErrorCreate": "Impossibile creare l'obiettivo", "targetErrorCreateDescription": "Si è verificato un errore durante la creazione del target", + "tlsServerName": "Nome Server Tls", + "tlsServerNameDescription": "Il nome del server TLS da usare per SNI", "save": "Salva", "proxyAdditional": "Impostazioni Proxy Aggiuntive", - "proxyAdditionalDescription": "Configura come la tua risorsa gestisce le impostazioni proxy", + "proxyAdditionalDescription": "Configura come la risorsa gestisce le impostazioni del proxy", "proxyCustomHeader": "Intestazione Host Personalizzata", "proxyCustomHeaderDescription": "L'intestazione host da impostare durante il proxy delle richieste. Lascia vuoto per usare quella predefinita.", "proxyAdditionalSubmit": "Salva Impostazioni Proxy", @@ -558,7 +572,7 @@ "rulesMatchType": "Tipo di Corrispondenza", "value": "Valore", "rulesAbout": "Informazioni sulle Regole", - "rulesAboutDescription": "Le regole ti permettono di controllare l'accesso alla tua risorsa in base a una serie di criteri. Puoi creare regole per consentire o negare l'accesso basato su indirizzo IP o percorso URL.", + "rulesAboutDescription": "Le regole consentono di controllare l'accesso alla risorsa in base a una serie di criteri. È possibile creare regole per consentire o negare l'accesso in base all'indirizzo IP o al percorso URL.", "rulesActions": "Azioni", "rulesActionAlwaysAllow": "Consenti Sempre: Ignora tutti i metodi di autenticazione", "rulesActionAlwaysDeny": "Nega Sempre: Blocca tutte le richieste; nessuna autenticazione può essere tentata", @@ -570,7 +584,7 @@ "rulesEnable": "Abilita Regole", "rulesEnableDescription": "Abilita o disabilita la valutazione delle regole per questa risorsa", "rulesResource": "Configurazione Regole Risorsa", - "rulesResourceDescription": "Configura le regole per controllare l'accesso alla tua risorsa", + "rulesResourceDescription": "Configura le regole per controllare l'accesso alla risorsa", "ruleSubmit": "Aggiungi Regola", "rulesNoOne": "Nessuna regola. Aggiungi una regola usando il modulo.", "rulesOrder": "Le regole sono valutate per priorità in ordine crescente.", @@ -586,7 +600,7 @@ "none": "Nessuno", "unknown": "Sconosciuto", "resources": "Risorse", - "resourcesDescription": "Le risorse sono proxy per le applicazioni in esecuzione sulla tua rete privata. Crea una risorsa per qualsiasi servizio HTTP/HTTPS o TCP/UDP raw sulla tua rete privata. Ogni risorsa deve essere collegata a un sito per abilitare la connettività privata e sicura attraverso un tunnel WireGuard crittografato.", + "resourcesDescription": "Le risorse sono proxy per applicazioni in esecuzione sulla rete privata. Crea una risorsa per qualsiasi servizio HTTP/HTTPS o TCP/UDP grezzo sulla tua rete privata. Ogni risorsa deve essere collegata a un sito per abilitare una connettività privata e sicura attraverso un tunnel WireGuard crittografato.", "resourcesWireGuardConnect": "Connettività sicura con crittografia WireGuard", "resourcesMultipleAuthenticationMethods": "Configura molteplici metodi di autenticazione", "resourcesUsersRolesAccess": "Controllo accessi basato su utenti e ruoli", @@ -597,7 +611,7 @@ "resourceSelect": "Seleziona risorsa", "shareLinks": "Link di Condivisione", "share": "Link Condivisibili", - "shareDescription2": "Crea link condivisibili per le tue risorse. I link forniscono accesso temporaneo o illimitato alla tua risorsa. Puoi configurare la durata di scadenza del link quando lo crei.", + "shareDescription2": "Crea link condivisibili alle risorse. I link forniscono un accesso temporaneo o illimitato alla tua risorsa. È possibile configurare la durata di scadenza del collegamento quando ne viene creato uno.", "shareEasyCreate": "Facile da creare e condividere", "shareConfigurableExpirationDuration": "Durata di scadenza configurabile", "shareSecureAndRevocable": "Sicuro e revocabile", @@ -607,19 +621,19 @@ "unknownCommand": "Comando sconosciuto", "newtErrorFetchReleases": "Impossibile recuperare le informazioni sulla versione: {err}", "newtErrorFetchLatest": "Errore nel recupero dell'ultima versione: {err}", - "newtEndpoint": "Endpoint Newt", - "newtId": "ID Newt", - "newtSecretKey": "Chiave Segreta Newt", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Segreto", "architecture": "Architettura", "sites": "Siti", - "siteWgAnyClients": "Usa qualsiasi client WireGuard per connetterti. Dovrai indirizzare le tue risorse interne usando l'IP del peer.", + "siteWgAnyClients": "Usa qualsiasi client WireGuard per connetterti. Dovrai indirizzare le risorse interne utilizzando l'IP del peer.", "siteWgCompatibleAllClients": "Compatibile con tutti i client WireGuard", "siteWgManualConfigurationRequired": "Configurazione manuale richiesta", "userErrorNotAdminOrOwner": "L'utente non è un amministratore o proprietario", "pangolinSettings": "Impostazioni - Pangolin", "accessRoleYour": "Il tuo ruolo:", - "accessRoleSelect2": "Seleziona un ruolo", - "accessUserSelect": "Seleziona un utente", + "accessRoleSelect2": "Seleziona ruoli", + "accessUserSelect": "Seleziona utenti", "otpEmailEnter": "Inserisci un'email", "otpEmailEnterDescription": "Premi invio per aggiungere un'email dopo averla digitata nel campo di input.", "otpEmailErrorInvalid": "Indirizzo email non valido. Il carattere jolly (*) deve essere l'intera parte locale.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Imposta Codice PIN", "resourcePincodeSetupTitleDescription": "Imposta un codice PIN per proteggere questa risorsa", "resourceRoleDescription": "Gli amministratori possono sempre accedere a questa risorsa.", - "resourceUsersRoles": "Utenti e Ruoli", + "resourceUsersRoles": "Controlli di Accesso", "resourceUsersRolesDescription": "Configura quali utenti e ruoli possono visitare questa risorsa", "resourceUsersRolesSubmit": "Salva Utenti e Ruoli", "resourceWhitelistSave": "Salvato con successo", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Trasferisci Risorsa", "siteDestination": "Sito Di Destinazione", "searchSites": "Cerca siti", + "countries": "Paesi", "accessRoleCreate": "Crea Ruolo", "accessRoleCreateDescription": "Crea un nuovo ruolo per raggruppare gli utenti e gestire i loro permessi.", "accessRoleCreateSubmit": "Crea Ruolo", @@ -766,15 +781,15 @@ "idpOidcConfigure": "Configurazione OAuth2/OIDC", "idpOidcConfigureDescription": "Configura gli endpoint e le credenziali del provider OAuth2/OIDC", "idpClientId": "ID Client", - "idpClientIdDescription": "L'ID client OAuth2 dal tuo provider di identità", + "idpClientIdDescription": "L'ID client OAuth2 dal provider di identità", "idpClientSecret": "Segreto Client", - "idpClientSecretDescription": "Il segreto client OAuth2 dal tuo provider di identità", + "idpClientSecretDescription": "Il segreto del client OAuth2 dal provider di identità", "idpAuthUrl": "URL di Autorizzazione", "idpAuthUrlDescription": "L'URL dell'endpoint di autorizzazione OAuth2", "idpTokenUrl": "URL del Token", "idpTokenUrlDescription": "L'URL dell'endpoint del token OAuth2", "idpOidcConfigureAlert": "Informazioni Importanti", - "idpOidcConfigureAlertDescription": "Dopo aver creato il provider di identità, dovrai configurare l'URL di callback nelle impostazioni del tuo provider di identità. L'URL di callback verrà fornito dopo la creazione riuscita.", + "idpOidcConfigureAlertDescription": "Dopo aver creato il provider di identità, è necessario configurare l'URL di callback nelle impostazioni del provider di identità. L'URL di callback verrà fornito dopo la creazione riuscita.", "idpToken": "Configurazione Token", "idpTokenDescription": "Configura come estrarre le informazioni dell'utente dal token ID", "idpJmespathAbout": "Informazioni su JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Crea Provider di Identità", "orgPolicies": "Politiche Organizzazione", "idpSettings": "Impostazioni {idpName}", - "idpCreateSettingsDescription": "Configura le impostazioni per il tuo provider di identità", + "idpCreateSettingsDescription": "Configura le impostazioni per il provider di identità", "roleMapping": "Mappatura Ruoli", "orgMapping": "Mappatura Organizzazione", "orgPoliciesSearch": "Cerca politiche organizzazione...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Provider di identità aggiornato con successo", "redirectUrl": "URL di Reindirizzamento", "redirectUrlAbout": "Informazioni sull'URL di Reindirizzamento", - "redirectUrlAboutDescription": "Questo è l'URL a cui gli utenti verranno reindirizzati dopo l'autenticazione. Devi configurare questo URL nelle impostazioni del tuo provider di identità.", + "redirectUrlAboutDescription": "Questo è l'URL a cui gli utenti saranno reindirizzati dopo l'autenticazione. È necessario configurare questo URL nelle impostazioni del provider di identità.", "pangolinAuth": "Autenticazione - Pangolina", "verificationCodeLengthRequirements": "Il tuo codice di verifica deve essere di 8 caratteri.", "errorOccurred": "Si è verificato un errore", @@ -909,6 +924,10 @@ "passwordResetSent": "Invieremo un codice di reset della password a questo indirizzo email.", "passwordResetCode": "Codice di Reset", "passwordResetCodeDescription": "Controlla la tua email per il codice di reset.", + "generatePasswordResetCode": "Genera Codice Di Ripristino Password", + "passwordResetCodeGenerated": "Codice Di Reimpostazione Password Generato", + "passwordResetCodeGeneratedDescription": "Condividi questo codice con l'utente. Possono usarlo per reimpostare la password.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nuova Password", "passwordNewConfirm": "Conferma Nuova Password", "changePassword": "Cambia Password", @@ -926,6 +945,9 @@ "pincodeAuth": "Codice Autenticatore", "pincodeSubmit2": "Invia Codice", "passwordResetSubmit": "Richiedi Reset", + "passwordResetAlreadyHaveCode": "Inserisci Il Codice Di Ripristino Della Password", + "passwordResetSmtpRequired": "Si prega di contattare l'amministratore", + "passwordResetSmtpRequiredDescription": "Per reimpostare la password è necessario un codice di reimpostazione della password. Si prega di contattare l'amministratore per assistenza.", "passwordBack": "Torna alla Password", "loginBack": "Torna al login", "signup": "Registrati", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Elenca Risorse del Sito", "actionUpdateSiteResource": "Aggiorna Risorsa del Sito", "actionListInvitations": "Elenco Inviti", + "actionExportLogs": "Esporta Log", + "actionViewLogs": "Visualizza Log", "noneSelected": "Nessuna selezione", "orgNotFound2": "Nessuna organizzazione trovata.", "searchProgress": "Ricerca...", "create": "Crea", "orgs": "Organizzazioni", "loginError": "Si è verificato un errore durante l'accesso", + "loginRequiredForDevice": "È richiesto il login per autenticare il dispositivo.", "passwordForgot": "Password dimenticata?", "otpAuth": "Autenticazione a Due Fattori", "otpAuthDescription": "Inserisci il codice dalla tua app di autenticazione o uno dei tuoi codici di backup monouso.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Home", "sidebarSites": "Siti", "sidebarResources": "Risorse", + "sidebarProxyResources": "Pubblico", + "sidebarClientResources": "Privato", "sidebarAccessControl": "Controllo Accesso", + "sidebarLogsAndAnalytics": "Registri E Analisi", "sidebarUsers": "Utenti", + "sidebarAdmin": "Amministratore", "sidebarInvitations": "Inviti", "sidebarRoles": "Ruoli", - "sidebarShareableLinks": "Collegamenti Condividibili", + "sidebarShareableLinks": "Collegamenti", "sidebarApiKeys": "Chiavi API", "sidebarSettings": "Impostazioni", "sidebarAllUsers": "Tutti Gli Utenti", "sidebarIdentityProviders": "Fornitori Di Identità", "sidebarLicense": "Licenza", "sidebarClients": "Client", + "sidebarUserDevices": "Utenti", + "sidebarMachineClients": "Macchine", "sidebarDomains": "Domini", + "sidebarGeneral": "Generale", + "sidebarLogAndAnalytics": "Log & Analytics", "sidebarBluePrints": "Progetti", + "sidebarOrganization": "Organizzazione", + "sidebarLogsAnalytics": "Analisi", "blueprints": "Progetti", "blueprintsDescription": "Applica le configurazioni dichiarative e visualizza le partite precedenti", "blueprintAdd": "Aggiungi Progetto", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Vedere il risultato del progetto applicato e gli eventuali errori verificatisi", "blueprintInfo": "Informazioni Sul Progetto", "message": "Messaggio", - "blueprintContentsDescription": "Definisci il contenuto di YAML che descrive la tua infrastruttura", + "blueprintContentsDescription": "Definire il contenuto YAML che descrive l'infrastruttura", "blueprintErrorCreateDescription": "Si è verificato un errore durante l'applicazione del progetto", "blueprintErrorCreate": "Errore nella creazione del progetto", "searchBlueprintProgress": "Cerca progetti...", @@ -1230,15 +1265,15 @@ "loading": "Caricamento", "restart": "Riavvia", "domains": "Domini", - "domainsDescription": "Gestisci domini per la tua organizzazione", + "domainsDescription": "Creare e gestire i domini disponibili nell'organizzazione", "domainsSearch": "Cerca domini...", "domainAdd": "Aggiungi Dominio", - "domainAddDescription": "Registra un nuovo dominio con la tua organizzazione", + "domainAddDescription": "Registra un nuovo dominio con all'organizzazione", "domainCreate": "Crea Dominio", "domainCreatedDescription": "Dominio creato con successo", "domainDeletedDescription": "Dominio eliminato con successo", - "domainQuestionRemove": "Sei sicuro di voler rimuovere il dominio dal tuo account?", - "domainMessageRemove": "Una volta rimosso, il dominio non sarà più associato al tuo account.", + "domainQuestionRemove": "Sei sicuro di voler rimuovere il dominio?", + "domainMessageRemove": "Una volta rimosso, il dominio non sarà più associato all'organizzazione.", "domainConfirmDelete": "Conferma Eliminazione Dominio", "domainDelete": "Elimina Dominio", "domain": "Dominio", @@ -1257,7 +1292,7 @@ "pending": "In attesa", "sidebarBilling": "Fatturazione", "billing": "Fatturazione", - "orgBillingDescription": "Gestisci le tue informazioni di fatturazione e abbonamenti", + "orgBillingDescription": "Gestisci le informazioni di fatturazione e gli abbonamenti", "github": "GitHub", "pangolinHosted": "Pangolin Hosted", "fossorial": "Fossoriale", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Aggiornamenti Prodotto", "productUpdateEmpty": "Nessun aggiornamento", "dismissAll": "Ignora tutto", - "pangolinUpdateAvailable": "Nuova versione disponibile", + "pangolinUpdateAvailable": "Aggiornamento Disponibile", "pangolinUpdateAvailableInfo": "La versione {version} è pronta per l'installazione", - "pangolinUpdateAvailableReleaseNotes": "Visualizza note di rilascio", + "pangolinUpdateAvailableReleaseNotes": "Visualizza Note Di Rilascio", "newtUpdateAvailable": "Aggiornamento Disponibile", "newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.", "domainPickerEnterDomain": "Dominio", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Controllando la disponibilità...", - "domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato. Prova un dominio diverso o verifica le impostazioni del dominio della tua organizzazione.", + "domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato. Prova un dominio diverso o controlla le impostazioni del dominio dell'organizzazione.", "domainPickerOrganizationDomains": "Domini dell'Organizzazione", "domainPickerProvidedDomains": "Domini Forniti", "domainPickerSubdomain": "Sottodominio: {subdomain}", @@ -1345,9 +1380,9 @@ "billingPortalError": "Errore del Portale", "billingDataUsageInfo": "Hai addebitato tutti i dati trasferiti attraverso i tunnel sicuri quando sei connesso al cloud. Questo include sia il traffico in entrata e in uscita attraverso tutti i siti. Quando si raggiunge il limite, i siti si disconnetteranno fino a quando non si aggiorna il piano o si riduce l'utilizzo. I dati non vengono caricati quando si utilizzano nodi.", "billingOnlineTimeInfo": "Ti viene addebitato in base al tempo in cui i tuoi siti rimangono connessi al cloud. Ad esempio, 44,640 minuti è uguale a un sito in esecuzione 24/7 per un mese intero. Quando raggiungi il tuo limite, i tuoi siti si disconnetteranno fino a quando non aggiorni il tuo piano o riduci l'utilizzo. Il tempo non viene caricato quando si usano i nodi.", - "billingUsersInfo": "Sei addebitato per ogni utente nella tua organizzazione. La fatturazione viene calcolata giornalmente in base al numero di account utente attivi nella tua organizzazione.", - "billingDomainInfo": "Sei addebitato per ogni dominio nella tua organizzazione. La fatturazione viene calcolata giornalmente in base al numero di account dominio attivi nella tua organizzazione.", - "billingRemoteExitNodesInfo": "Sei addebitato per ogni nodo gestito nella tua organizzazione. La fatturazione viene calcolata giornalmente in base al numero di nodi gestiti attivi nella tua organizzazione.", + "billingUsersInfo": "Sei addebitato per ogni utente nell'organizzazione. La fatturazione viene calcolata quotidianamente in base al numero di account utente attivi nel tuo org.", + "billingDomainInfo": "Sei addebitato per ogni dominio nell'organizzazione. La fatturazione viene calcolata quotidianamente in base al numero di account di dominio attivi nel tuo org.", + "billingRemoteExitNodesInfo": "Sei addebitato per ogni nodo gestito nell'organizzazione. La fatturazione viene calcolata quotidianamente in base al numero di nodi gestiti attivi nel tuo org.", "domainNotFound": "Domini Non Trovati", "domainNotFoundDescription": "Questa risorsa è disabilitata perché il dominio non esiste più nel nostro sistema. Si prega di impostare un nuovo dominio per questa risorsa.", "failed": "Fallito", @@ -1430,29 +1465,32 @@ "and": "e", "privacyPolicy": "informativa sulla privacy" }, + "signUpMarketing": { + "keepMeInTheLoop": "Tienimi in loop con notizie, aggiornamenti e nuove funzionalità via e-mail." + }, "siteRequired": "Il sito è richiesto.", "olmTunnel": "Tunnel Olm", "olmTunnelDescription": "Usa Olm per la connettività client", "errorCreatingClient": "Errore nella creazione del client", "clientDefaultsNotFound": "Impostazioni predefinite del client non trovate", "createClient": "Crea Cliente", - "createClientDescription": "Crea un nuovo cliente per connettersi ai tuoi siti", + "createClientDescription": "Crea un nuovo client per accedere alle risorse private", "seeAllClients": "Vedi Tutti i Clienti", "clientInformation": "Informazioni sul Cliente", "clientNamePlaceholder": "Nome Cliente", "address": "Indirizzo", "subnetPlaceholder": "Sottorete", - "addressDescription": "L'indirizzo che questo cliente utilizzerà per la connettività", + "addressDescription": "L'indirizzo interno del client. Deve rientrare nella sottorete dell'organizzazione.", "selectSites": "Seleziona siti", "sitesDescription": "Il cliente avrà connettività ai siti selezionati", "clientInstallOlm": "Installa Olm", "clientInstallOlmDescription": "Avvia Olm sul tuo sistema", - "clientOlmCredentials": "Credenziali Olm", - "clientOlmCredentialsDescription": "Ecco come Olm si autenticherà con il server", - "olmEndpoint": "Endpoint Olm", - "olmId": "ID Olm", - "olmSecretKey": "Chiave Segreta Olm", - "clientCredentialsSave": "Salva le Tue Credenziali", + "clientOlmCredentials": "Credenziali", + "clientOlmCredentialsDescription": "Questo è il modo in cui il client si autenticerà con il server", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Segreto", + "clientCredentialsSave": "Salva le credenziali", "clientCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.", "generalSettingsDescription": "Configura le impostazioni generali per questo cliente", "clientUpdated": "Cliente aggiornato", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Si è verificato un errore durante il recupero dei siti.", "olmErrorFetchReleases": "Si è verificato un errore durante il recupero delle versioni di Olm.", "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 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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Monitorare lo stato di salute di questo obiettivo. Se necessario, è possibile monitorare un endpoint diverso da quello del bersaglio.", "healthScheme": "Metodo", "healthSelectScheme": "Seleziona Metodo", + "healthCheckPortInvalid": "La porta di controllo dello stato di salute deve essere compresa tra 1 e 65535", "healthCheckPath": "Percorso", "healthHostname": "IP / Nome host", "healthPort": "Porta", "healthCheckPathDescription": "Percorso per verificare lo stato di salute.", - "healthyIntervalSeconds": "Intervallo Sano", - "unhealthyIntervalSeconds": "Intervallo Non Sano", + "healthyIntervalSeconds": "Intervallo Sano (Sec)", + "unhealthyIntervalSeconds": "Intervallo Non Sano (Sec)", "IntervalSeconds": "Intervallo Sano", - "timeoutSeconds": "Timeout", + "timeoutSeconds": "Timeout (sec)", "timeIsInSeconds": "Il tempo è in secondi", "retryAttempts": "Tentativi di Riprova", "expectedResponseCodes": "Codici di Risposta Attesi", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Modifica Dominio", "siteName": "Nome del Sito", "proxyPort": "Porta", - "resourcesTableProxyResources": "Risorse Proxy", - "resourcesTableClientResources": "Risorse Client", + "resourcesTableProxyResources": "Pubblico", + "resourcesTableClientResources": "Privato", "resourcesTableNoProxyResourcesFound": "Nessuna risorsa proxy trovata.", "resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.", "resourcesTableDestination": "Destinazione", - "resourcesTableTheseResourcesForUseWith": "Queste risorse sono per uso con", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Client", "resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.", "resourcesTableNoTargets": "Nessun obiettivo", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Offline", "resourcesTableUnknown": "Sconosciuto", "resourcesTableNotMonitored": "Non monitorato", - "editInternalResourceDialogEditClientResource": "Modifica Risorsa Client", - "editInternalResourceDialogUpdateResourceProperties": "Aggiorna le proprietà della risorsa e la configurazione del target per {resourceName}.", + "editInternalResourceDialogEditClientResource": "Modifica Risorse Private", + "editInternalResourceDialogUpdateResourceProperties": "Aggiorna la configurazione delle risorse e i controlli di accesso per {resourceName}", "editInternalResourceDialogResourceProperties": "Proprietà della Risorsa", "editInternalResourceDialogName": "Nome", "editInternalResourceDialogProtocol": "Protocollo", @@ -1560,11 +1597,22 @@ "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", + "editInternalResourceDialogPortModeRequired": "Protocollo, porta proxy e porta di destinazione sono richiesti per la modalità porta", + "editInternalResourceDialogMode": "Modalità", + "editInternalResourceDialogModePort": "Porta", + "editInternalResourceDialogModeHost": "Host", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Destinazione", + "editInternalResourceDialogDestinationHostDescription": "L'indirizzo IP o il nome host della risorsa nella rete del sito.", + "editInternalResourceDialogDestinationIPDescription": "L'indirizzo IP o hostname della risorsa nella rete del sito.", + "editInternalResourceDialogDestinationCidrDescription": "La gamma CIDR della risorsa sulla rete del sito.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Un alias DNS interno opzionale per questa risorsa.", "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.", + "createInternalResourceDialogCreateClientResource": "Crea Risorsa Privata", + "createInternalResourceDialogCreateClientResourceDescription": "Crea una nuova risorsa che sarà accessibile solo ai client connessi all'organizzazione", "createInternalResourceDialogResourceProperties": "Proprietà della Risorsa", "createInternalResourceDialogName": "Nome", "createInternalResourceDialogSite": "Sito", @@ -1593,11 +1641,22 @@ "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", + "createInternalResourceDialogPortModeRequired": "Protocollo, porta proxy e porta di destinazione sono richiesti per la modalità porta", + "createInternalResourceDialogMode": "Modalità", + "createInternalResourceDialogModePort": "Porta", + "createInternalResourceDialogModeHost": "Host", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Destinazione", + "createInternalResourceDialogDestinationHostDescription": "L'indirizzo IP o il nome host della risorsa nella rete del sito.", + "createInternalResourceDialogDestinationCidrDescription": "La gamma CIDR della risorsa sulla rete del sito.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Un alias DNS interno opzionale per questa risorsa.", "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.", + "siteAcceptClientConnectionsDescription": "Consenti ai dispositivi utente e ai client di accedere alle risorse di questo sito. Questo può essere modificato in seguito.", + "siteAddress": "Indirizzo Del Sito (Avanzato)", + "siteAddressDescription": "L'indirizzo interno del sito. Deve rientrare nella sottorete dell'organizzazione.", + "siteNameDescription": "Il nome visualizzato del sito che può essere modificato in seguito.", "autoLoginExternalIdp": "Accesso Automatico con IDP Esterno", "autoLoginExternalIdpDescription": "Reindirizzare immediatamente l'utente all'IDP esterno per l'autenticazione.", "selectIdp": "Seleziona IDP", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Tipo Provider Identità", "roleMappingExpressionPlaceholder": "es. contiene(gruppi, 'admin') && 'Admin' 'Membro'", "idpGoogleConfiguration": "Configurazione Google", - "idpGoogleConfigurationDescription": "Configura le tue credenziali di Google OAuth2", - "idpGoogleClientIdDescription": "Il Tuo Client Id Google OAuth2", - "idpGoogleClientSecretDescription": "Il Tuo Client Google OAuth2 Secret", + "idpGoogleConfigurationDescription": "Configura le credenziali di Google OAuth2", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Google OAuth2 Client Secret", "idpAzureConfiguration": "Configurazione Azure Entra ID", "idpAzureConfigurationDescription": "Configura le credenziali OAuth2 di Azure Entra ID", "idpTenantId": "ID Tenant", - "idpTenantIdPlaceholder": "iltuo-inquilino-id", - "idpAzureTenantIdDescription": "Il tuo ID del tenant Azure (trovato nella panoramica di Azure Active Directory)", - "idpAzureClientIdDescription": "Il Tuo Id Client Registrazione App Azure", - "idpAzureClientSecretDescription": "Il Tuo Client Di Registrazione App Azure Secret", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "Azure tenant ID (trovato nella panoramica Azure Active Directory)", + "idpAzureClientIdDescription": "Azure App Id Registrazione Client", + "idpAzureClientSecretDescription": "Azure App Registrazione Client Segreto", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Configurazione Google", "idpAzureConfigurationTitle": "Configurazione Azure Entra ID", "idpTenantIdLabel": "ID Tenant", - "idpAzureClientIdDescription2": "Il Tuo Id Client Registrazione App Azure", - "idpAzureClientSecretDescription2": "Il Tuo Client Di Registrazione App Azure Secret", + "idpAzureClientIdDescription2": "Azure App Id Registrazione Client", + "idpAzureClientSecretDescription2": "Azure App Registrazione Client Segreto", "idpGoogleDescription": "Google OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Sottorete", "subnetDescription": "La sottorete per la configurazione di rete di questa organizzazione.", "authPage": "Pagina Autenticazione", - "authPageDescription": "Configura la pagina di autenticazione per la tua organizzazione", + "authPageDescription": "Configura la pagina di autenticazione per l'organizzazione", "authPageDomain": "Dominio Pagina Auth", "noDomainSet": "Nessun dominio impostato", "changeDomain": "Cambia Dominio", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Imposta Dominio Pagina Autenticazione", "failedToFetchCertificate": "Recupero del certificato non riuscito", "failedToRestartCertificate": "Riavvio del certificato non riuscito", - "addDomainToEnableCustomAuthPages": "Aggiungi un dominio per abilitare le pagine di autenticazione personalizzate per la tua organizzazione", + "addDomainToEnableCustomAuthPages": "Aggiungi un dominio per abilitare le pagine di autenticazione personalizzate per l'organizzazione", "selectDomainForOrgAuthPage": "Seleziona un dominio per la pagina di autenticazione dell'organizzazione", "domainPickerProvidedDomain": "Dominio Fornito", "domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" non può essere reso valido per {domain}.", "domainPickerSubdomainSanitized": "Sottodominio igienizzato", "domainPickerSubdomainCorrected": "\"{sub}\" è stato corretto in \"{sanitized}\"", - "orgAuthSignInTitle": "Accedi alla tua organizzazione", + "orgAuthSignInTitle": "Accedi all'organizzazione", "orgAuthChooseIdpDescription": "Scegli il tuo provider di identità per continuare", "orgAuthNoIdpConfigured": "Questa organizzazione non ha nessun provider di identità configurato. Puoi accedere con la tua identità Pangolin.", "orgAuthSignInWithPangolin": "Accedi con Pangolino", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Abilita autenticazione a due fattori", "completeSecuritySteps": "Passi Di Sicurezza Completa", "securitySettings": "Impostazioni Di Sicurezza", - "securitySettingsDescription": "Configura i criteri di sicurezza per la tua organizzazione", + "securitySettingsDescription": "Configura i criteri di sicurezza per l'organizzazione", "requireTwoFactorForAllUsers": "Richiede l'autenticazione a due fattori per tutti gli utenti", "requireTwoFactorDescription": "Se abilitata, tutti gli utenti interni di questa organizzazione devono avere un'autenticazione a due fattori abilitata per accedere all'organizzazione.", "requireTwoFactorDisabledDescription": "Questa funzione richiede una licenza valida (Enterprise) o un abbonamento attivo (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Enterprise Edition", "unlicensed": "Senza Licenza", "beta": "Beta", - "manageClients": "Gestisci Clienti", - "manageClientsDescription": "I client sono dispositivi che possono connettersi ai tuoi siti", + "manageUserDevices": "Dispositivi Utente", + "manageUserDevicesDescription": "Visualizza e gestisci i dispositivi che gli utenti utilizzano per connettersi privatamente alle risorse", + "manageMachineClients": "Gestisci Client Machine", + "manageMachineClientsDescription": "Creare e gestire client che server e sistemi utilizzano per connettersi privatamente alle risorse", + "clientsTableUserClients": "Utente", + "clientsTableMachineClients": "Macchina", "licenseTableValidUntil": "Valido Fino A", "saasLicenseKeysSettingsTitle": "Licenze Enterprise", "saasLicenseKeysSettingsDescription": "Genera e gestisci le chiavi di licenza Enterprise per le istanze di Pangolin self-hosted", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "striscia", "sidebarEnableEnterpriseLicense": "Abilita Licenza Enterprise", "cannotbeUndone": "Questo non può essere annullato.", - "toConfirm": "per confermare", + "toConfirm": "per confermare.", "deleteClientQuestion": "Sei sicuro di voler rimuovere il client dal sito e dall'organizzazione?", "clientMessageRemove": "Una volta rimosso, il client non sarà più in grado di connettersi al sito.", "sidebarLogs": "Registri", "request": "Richiesta", + "requests": "Richieste", "logs": "Registri", "logsSettingsDescription": "Monitora i registri raccolti da questa orginizzazione", "searchLogs": "Cerca registro...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Motivo", "requestLogs": "Log Richiesta", + "requestAnalytics": "Richiedi Analisi", "host": "Host", "location": "Posizione", "actionLogs": "Log Azioni", @@ -2029,6 +2094,7 @@ "logRetention": "Ritenzione Registro", "logRetentionDescription": "Gestisci per quanto tempo i diversi tipi di log sono mantenuti per questa organizzazione o disabilitali", "requestLogsDescription": "Visualizza i registri di richiesta dettagliati per le risorse in questa organizzazione", + "requestAnalyticsDescription": "Visualizza le analisi dettagliate della richiesta per le risorse in questa organizzazione", "logRetentionRequestLabel": "Richiedi Ritenzione Log", "logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste", "logRetentionAccessLabel": "Ritenzione Registro Accesso", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 giorni", "logRetention90Days": "90 giorni", "logRetentionForever": "Per Sempre", + "logRetentionEndOfFollowingYear": "Fine dell'anno successivo", "actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione", "accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione", "licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza Enterprise.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Preferisci Certificato Wildcard", "unverified": "Non Verificato", "domainSetting": "Impostazioni Dominio", - "domainSettingDescription": "Configura le impostazioni per il tuo dominio", + "domainSettingDescription": "Configura le impostazioni per il dominio", "preferWildcardCertDescription": "Tentativo di generare un certificato jolly (richiede un risolutore di certificati correttamente configurato).", "recordName": "Nome Record", "auto": "Automatico", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "È disponibile una versione aggiornata di Olm. Si prega di aggiornare all'ultima versione per la migliore esperienza.", "client": "Client", "proxyProtocol": "Impostazioni Protocollo Proxy", - "proxyProtocolDescription": "Configurare il protocollo proxy per preservare gli indirizzi IP client per i servizi TCP/UDP.", + "proxyProtocolDescription": "Configurare il protocollo proxy per preservare gli indirizzi IP client per i servizi TCP.", "enableProxyProtocol": "Abilita Protocollo Proxy", - "proxyProtocolInfo": "Conserva gli indirizzi IP del client per i backend TCP/UDP", + "proxyProtocolInfo": "Conserva gli indirizzi IP del client per i backend TCP", "proxyProtocolVersion": "Versione Protocollo Proxy", "version1": " Versione 1 (Consigliato)", "version2": "Versione 2", "versionDescription": "La versione 1 è testuale e ampiamente supportata. La versione 2 è binaria e più efficiente, ma meno compatibile.", "warning": "Attenzione", - "proxyProtocolWarning": "La tua applicazione backend deve essere configurata per accettare le connessioni del protocollo proxy. Se il tuo backend non supporta il protocollo proxy, abilitando questa opzione si interromperanno tutte le connessioni. Assicurati di configurare il tuo backend per fidarti delle intestazioni del protocollo proxy da Traefik.", + "proxyProtocolWarning": "L'applicazione backend deve essere configurata per accettare le connessioni del protocollo proxy. Se il tuo backend non supporta il protocollo proxy, abilitarlo interromperà tutte le connessioni, quindi attivalo solo se sai cosa stai facendo. Assicurati di configurare il tuo backend per fidarti delle intestazioni del protocollo proxy da Traefik.", "restarting": "Riavvio...", "manual": "Manuale", "messageSupport": "Supporto Messaggio", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Messaggio Inviato!", "supportWillContact": "Saremo in contatto a breve!", "selectLogRetention": "Seleziona ritenzione log", + "terms": "Termini", + "privacy": "Privacy", + "security": "Sicurezza", + "docs": "Documenti", + "deviceActivation": "Attivazione dispositivo", + "deviceCodeInvalidFormat": "Il codice deve contenere 9 caratteri (es. A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Codice non valido o scaduto", + "deviceCodeVerifyFailed": "Impossibile verificare il codice del dispositivo", + "signedInAs": "Accesso come", + "deviceCodeEnterPrompt": "Inserisci il codice visualizzato sul dispositivo", + "continue": "Continua", + "deviceUnknownLocation": "Posizione sconosciuta", + "deviceAuthorizationRequested": "Questa autorizzazione è stata richiesta ad {location} su {date}. Assicurati di fidarti di questo dispositivo in quanto avrà accesso all'account.", + "deviceLabel": "Dispositivo: {deviceName}", + "deviceWantsAccess": "vuole accedere al tuo account", + "deviceExistingAccess": "Accesso esistente:", + "deviceFullAccess": "Accesso completo al tuo account", + "deviceOrganizationsAccess": "Accesso a tutte le organizzazioni a cui il tuo account ha accesso", + "deviceAuthorize": "Autorizza {applicationName}", + "deviceConnected": "Dispositivo Connesso!", + "deviceAuthorizedMessage": "Il dispositivo è autorizzato ad accedere al tuo account.", + "pangolinCloud": "Pangolin Cloud", + "viewDevices": "Visualizza Dispositivi", + "viewDevicesDescription": "Gestisci i tuoi dispositivi connessi", + "noDevices": "Nessun dispositivo trovato", + "dateCreated": "Data Di Creazione", + "unnamedDevice": "Dispositivo Senza Nome", + "deviceQuestionRemove": "Sei sicuro di voler eliminare questo dispositivo?", + "deviceMessageRemove": "Questa azione non può essere annullata.", + "deviceDeleteConfirm": "Elimina Dispositivo", + "deleteDevice": "Elimina Dispositivo", + "errorLoadingDevices": "Errore nel caricamento dei dispositivi", + "failedToLoadDevices": "Impossibile caricare i dispositivi", + "deviceDeleted": "Dispositivo eliminato", + "deviceDeletedDescription": "Il dispositivo è stato eliminato con successo.", + "errorDeletingDevice": "Errore nell'eliminare il dispositivo", + "failedToDeleteDevice": "Impossibile eliminare il dispositivo", "showColumns": "Mostra Colonne", "hideColumns": "Nascondi Colonne", "columnVisibility": "Visibilità Colonna", @@ -2111,10 +2215,14 @@ "enableSelected": "Abilita Selezionati", "disableSelected": "Disabilita Selezionati", "checkSelectedStatus": "Controlla lo stato dei selezionati", + "clients": "Client", + "accessClientSelect": "Seleziona client macchina", + "resourceClientDescription": "Clienti di macchine che possono accedere a questa risorsa", + "regenerate": "Rigenera", "credentials": "Credenziali", "savecredentials": "Salva Credenziali", - "regeneratecredentials": "Ri-chiave", - "regenerateCredentials": "Rigenera e salva le tue credenziali", + "regenerateCredentialsButton": "Rigenera Credenziali", + "regenerateCredentials": "Rigenera Credenziali", "generatedcredentials": "Credenziali Generate", "copyandsavethesecredentials": "Copia e salva queste credenziali", "copyandsavethesecredentialsdescription": "Queste credenziali non verranno mostrate di nuovo dopo aver lasciato questa pagina. Salvarle in modo sicuro ora.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Le credenziali sono state rigenerate e salvate con successo.", "credentialsSaveError": "Errore Di Salvataggio Credenziali", "credentialsSaveErrorDescription": "Errore durante la rigenerazione e il salvataggio delle credenziali.", - "regenerateCredentialsWarning": "Rigenerare le credenziali invaliderà quelle precedenti. Assicurarsi di aggiornare le configurazioni che utilizzano queste credenziali.", + "regenerateCredentialsWarning": "Rigenerare le credenziali invaliderà quelle precedenti e causerà una disconnessione. Assicurarsi di aggiornare le configurazioni che utilizzano queste credenziali.", "confirm": "Conferma", "regenerateCredentialsConfirmation": "Sei sicuro di voler rigenerare le credenziali?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Chiave Segreta", - "featureDisabledTooltip": "Questa funzione è disponibile solo nel piano aziendale e richiede una licenza per utilizzarla.", "niceId": "Simpatico ID", "niceIdUpdated": "Nice ID Aggiornato", "niceIdUpdatedSuccessfully": "Nizza Id Aggiornato Con Successo", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Si è verificato un errore durante l'aggiornamento del Nice ID.", "niceIdCannotBeEmpty": "Il Nice ID non può essere vuoto", "enterIdentifier": "Inserisci identificatore", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "Non tu? Usa un account diverso.", + "deviceLoginDeviceRequestingAccessToAccount": "Un dispositivo sta richiedendo l'accesso a questo account.", + "noData": "Nessun Dato", + "machineClients": "Machine Clients", + "install": "Installa", + "run": "Esegui", + "clientNameDescription": "Il nome visualizzato del client che può essere modificato in seguito.", + "clientAddress": "Indirizzo Client (Avanzato)", + "setupFailedToFetchSubnet": "Recupero della sottorete predefinita non riuscito", + "setupSubnetAdvanced": "Subnet (avanzato)", + "setupSubnetDescription": "La subnet per la rete interna di questa organizzazione.", + "siteRegenerateAndDisconnect": "Rigenera e disconnetti", + "siteRegenerateAndDisconnectConfirmation": "Sei sicuro di voler rigenerare le credenziali e disconnettere questo sito?", + "siteRegenerateAndDisconnectWarning": "Questo rigenererà le credenziali e disconnetterà immediatamente il sito. Il sito dovrà essere riavviato con le nuove credenziali.", + "siteRegenerateCredentialsConfirmation": "Sei sicuro di voler rigenerare le credenziali per questo sito?", + "siteRegenerateCredentialsWarning": "Questo rigenererà le credenziali. Il sito rimarrà connesso finché non lo riavvierai manualmente e userai le nuove credenziali.", + "clientRegenerateAndDisconnect": "Rigenera e disconnetti", + "clientRegenerateAndDisconnectConfirmation": "Sei sicuro di voler rigenerare le credenziali e disconnettere questo client?", + "clientRegenerateAndDisconnectWarning": "Questo rigenererà le credenziali e disconnetterà immediatamente il client. Il client dovrà essere riavviato con le nuove credenziali.", + "clientRegenerateCredentialsConfirmation": "Sei sicuro di voler rigenerare le credenziali per questo client?", + "clientRegenerateCredentialsWarning": "Questo rigenererà le credenziali. Il client rimarrà connesso fino a quando non lo riavvierai manualmente e userai le nuove credenziali.", + "remoteExitNodeRegenerateAndDisconnect": "Rigenera e disconnetti", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Sei sicuro di voler rigenerare le credenziali e disconnettere questo nodo di uscita remoto?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Questo rigenererà le credenziali e disconnetterà immediatamente il nodo di uscita remoto. Il nodo di uscita remoto dovrà essere riavviato con le nuove credenziali.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Sei sicuro di voler rigenerare le credenziali per questo nodo di uscita remoto?", + "remoteExitNodeRegenerateCredentialsWarning": "Questo rigenererà le credenziali. Il nodo di uscita remoto rimarrà connesso finché non lo riavvierai manualmente e userai le nuove credenziali.", + "agent": "Agente" } diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 2ce7f7e6..82a12e4e 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -1,12 +1,12 @@ { - "setupCreate": "조직, 사이트 및 리소스를 생성하십시오.", + "setupCreate": "조직, 사이트 및 리소스를 생성합니다.", "setupNewOrg": "새 조직", "setupCreateOrg": "조직 생성", "setupCreateResources": "리소스 생성", "setupOrgName": "조직 이름", - "orgDisplayName": "이것은 귀하의 조직의 표시 이름입니다.", + "orgDisplayName": "이것은 조직의 표시 이름입니다.", "orgId": "조직 ID", - "setupIdentifierMessage": "이것은 귀하의 조직에 대한 고유 식별자입니다. 표시 이름과는 별개입니다.", + "setupIdentifierMessage": "이것은 조직의 고유 식별자입니다.", "setupErrorIdentifier": "조직 ID가 이미 사용 중입니다. 다른 것을 선택해 주세요.", "componentsErrorNoMemberCreate": "현재 어떤 조직의 구성원도 아닙니다. 시작하려면 조직을 생성하세요.", "componentsErrorNoMember": "현재 어떤 조직의 구성원도 아닙니다.", @@ -50,10 +50,10 @@ "siteMessageRemove": "삭제되면 사이트에 더 이상 액세스할 수 없습니다. 사이트와 연결된 모든 대상도 삭제됩니다.", "siteQuestionRemove": "조직에서 사이트를 제거하시겠습니까?", "siteManageSites": "사이트 관리", - "siteDescription": "안전한 터널을 통해 네트워크에 연결할 수 있도록 허용", + "siteDescription": "프라이빗 네트워크로의 연결을 활성화하려면 사이트를 생성하고 관리하세요.", "siteCreate": "사이트 생성", "siteCreateDescription2": "아래 단계를 따라 새 사이트를 생성하고 연결하십시오", - "siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하십시오.", + "siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하세요.", "close": "닫기", "siteErrorCreate": "사이트 생성 오류", "siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다", @@ -74,7 +74,7 @@ "siteInstallNewt": "Newt 설치", "siteInstallNewtDescription": "시스템에서 Newt 실행하기", "WgConfiguration": "WireGuard 구성", - "WgConfigurationDescription": "네트워크에 연결하기 위한 다음 구성을 사용하십시오.", + "WgConfigurationDescription": "네트워크에 연결하기 위한 다음 구성을 사용하세요.", "operatingSystem": "운영 체제", "commands": "명령", "recommended": "추천", @@ -87,25 +87,25 @@ "siteUpdated": "사이트가 업데이트되었습니다", "siteUpdatedDescription": "사이트가 업데이트되었습니다.", "siteGeneralDescription": "이 사이트에 대한 일반 설정을 구성하세요.", - "siteSettingDescription": "사이트에서 설정을 구성하세요", + "siteSettingDescription": "사이트에서 설정을 구성하세요.", "siteSetting": "{siteName} 설정", - "siteNewtTunnel": "뉴트 터널 (추천)", - "siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.", + "siteNewtTunnel": "뉴트 사이트 (추천)", + "siteNewtTunnelDescription": "네트워크의 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.", "siteWg": "기본 WireGuard", "siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.", "siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.", "siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.", "siteLocalDescriptionSaas": "로컬 리소스 전용. 터널링 금지. 원격 노드에서만 사용 가능합니다.", "siteSeeAll": "모든 사이트 보기", - "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요", - "siteNewtCredentials": "Newt 자격 증명", - "siteNewtCredentialsDescription": "이것이 Newt가 서버와 인증하는 방법입니다", + "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요.", + "siteNewtCredentials": "자격 증명", + "siteNewtCredentialsDescription": "이것이 사이트가 서버와 인증하는 방법입니다.", "siteCredentialsSave": "자격 증명 저장", "siteCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", "siteInfo": "사이트 정보", "status": "상태", "shareTitle": "공유 링크 관리", - "shareDescription": "공유 가능한 링크를 생성하여 리소스에 대한 임시 또는 영구 액세스를 부여합니다.", + "shareDescription": "공유 가능한 링크를 생성하여 프록시 리소스에 임시 또는 영구적으로 액세스하세요.", "shareSearch": "공유 링크 검색...", "shareCreate": "공유 링크 생성", "shareErrorDelete": "링크 삭제에 실패했습니다.", @@ -144,8 +144,10 @@ "expires": "만료", "never": "절대", "shareErrorSelectResource": "리소스를 선택하세요", - "resourceTitle": "리소스 관리", - "resourceDescription": "개인 애플리케이션에 대한 보안 프록시 생성", + "proxyResourceTitle": "공개 리소스 관리", + "proxyResourceDescription": "웹 브라우저를 통해 공용으로 접근할 수 있는 리소스를 생성하고 관리하세요.", + "clientResourceTitle": "개인 리소스 관리", + "clientResourceDescription": "연결된 클라이언트를 통해서만 접근할 수 있는 리소스를 생성하고 관리하세요.", "resourcesSearch": "리소스 검색...", "resourceAdd": "리소스 추가", "resourceErrorDelte": "리소스 삭제 중 오류 발생", @@ -155,9 +157,9 @@ "resourceMessageRemove": "제거되면 리소스에 더 이상 접근할 수 없습니다. 리소스와 연결된 모든 대상도 제거됩니다.", "resourceQuestionRemove": "조직에서 리소스를 제거하시겠습니까?", "resourceHTTP": "HTTPS 리소스", - "resourceHTTPDescription": "서브도메인 또는 기본 도메인을 사용하여 HTTPS를 통해 앱에 대한 요청을 프록시합니다.", + "resourceHTTPDescription": "서브도메인이나 기본 도메인을 사용하여 HTTPS를 통해 앱에 대한 요청을 프록시합니다.", "resourceRaw": "원시 TCP/UDP 리소스", - "resourceRawDescription": "TCP/UDP를 통해 포트 번호를 사용하여 앱에 요청을 프록시합니다.", + "resourceRawDescription": "TCP/UDP를 사용하여 포트 번호를 통해 앱에 요청을 프록시합니다. 이 기능은 사이트가 노드에 연결될 때만 작동합니다.", "resourceCreate": "리소스 생성", "resourceCreateDescription": "아래 단계를 따라 새 리소스를 생성하세요.", "resourceSeeAll": "모든 리소스 보기", @@ -171,22 +173,22 @@ "noCountryFound": "국가를 찾을 수 없습니다.", "siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.", "resourceType": "리소스 유형", - "resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요", + "resourceTypeDescription": "리소스에 액세스하는 방법을 결정하세요.", "resourceHTTPSSettings": "HTTPS 설정", - "resourceHTTPSSettingsDescription": "리소스에 대한 HTTPS 접근 방식을 구성하십시오.", + "resourceHTTPSSettingsDescription": "리소스가 HTTPS로 접근할 수 있는 방식을 구성합니다.", "domainType": "도메인 유형", "subdomain": "서브도메인", "baseDomain": "기본 도메인", "subdomnainDescription": "리소스에 접근할 수 있는 하위 도메인입니다.", "resourceRawSettings": "TCP/UDP 설정", - "resourceRawSettingsDescription": "리소스를 TCP/UDP를 통해 액세스하는 방법을 구성합니다. 리소스를 호스트 Pangolin 서버의 포트에 매핑하여 서버-public-ip:매핑된 포트에서 리소스에 액세스할 수 있습니다.", + "resourceRawSettingsDescription": "TCP/UDP를 통해 리소스에 접근하는 방법을 구성하세요.", "protocol": "프로토콜", "protocolSelect": "프로토콜 선택", "resourcePortNumber": "포트 번호", "resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.", "cancel": "취소", "resourceConfig": "구성 스니펫", - "resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣으십시오.", + "resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣습니다.", "resourceAddEntrypoints": "Traefik: 엔트리포인트 추가", "resourceExposePorts": "Gerbil: Docker Compose에서 포트 노출", "resourceLearnRaw": "TCP/UDP 리소스 구성 방법 알아보기", @@ -204,10 +206,10 @@ "rules": "규칙", "resourceSettingDescription": "리소스의 설정을 구성하세요.", "resourceSetting": "{resourceName} 설정", - "alwaysAllow": "항상 허용", - "alwaysDeny": "항상 거부", + "alwaysAllow": "인증 우회", + "alwaysDeny": "접근 차단", "passToAuth": "인증으로 전달", - "orgSettingsDescription": "조직의 일반 설정을 구성하세요", + "orgSettingsDescription": "조직의 일반 설정을 구성하세요.", "orgGeneralSettings": "조직 설정", "orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.", "saveGeneralSettings": "일반 설정 저장", @@ -232,7 +234,7 @@ "orgMissing": "조직 ID가 누락되었습니다", "orgMissingMessage": "조직 ID 없이 초대장을 재생성할 수 없습니다.", "accessUsersManage": "사용자 관리", - "accessUsersDescription": "사용자를 초대하고 역할에 추가하여 조직에 대한 접근을 관리하세요", + "accessUsersDescription": "이 조직에 액세스할 사용자 초대 및 관리", "accessUsersSearch": "사용자 검색...", "accessUserCreate": "사용자 생성", "accessUserRemove": "사용자 제거", @@ -241,13 +243,13 @@ "role": "역할", "nameRequired": "이름은 필수입니다", "accessRolesManage": "역할 관리", - "accessRolesDescription": "조직에 대한 액세스를 관리할 역할 구성", + "accessRolesDescription": "조직의 사용자에 대한 역할을 생성하고 관리합니다.", "accessRolesSearch": "역할 검색...", "accessRolesAdd": "역할 추가", "accessRoleDelete": "역할 삭제", "description": "설명", "inviteTitle": "열린 초대", - "inviteDescription": "다른 사용자에 대한 초대를 관리하세요", + "inviteDescription": "다른 사용자가 조직에 참여하도록 초대장을 관리합니다.", "inviteSearch": "초대 검색...", "minutes": "분", "hours": "시간", @@ -264,10 +266,10 @@ "apiKeysCreateDescription": "조직을 위한 새로운 API 키 생성", "apiKeysGeneralSettings": "권한", "apiKeysGeneralSettingsDescription": "이 API 키가 수행할 수 있는 작업 결정", - "apiKeysList": "귀하의 API 키", + "apiKeysList": "새로운 API 키", "apiKeysSave": "API 키 저장", "apiKeysSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", - "apiKeysInfo": "귀하의 API 키는 다음과 같습니다:", + "apiKeysInfo": "API 키는 다음과 같습니다:", "apiKeysConfirmCopy": "API 키를 복사했습니다", "generate": "생성", "done": "완료", @@ -424,7 +426,7 @@ "userCreated": "사용자가 생성되었습니다.", "userCreatedDescription": "사용자가 성공적으로 생성되었습니다.", "userTypeInternal": "내부 사용자", - "userTypeInternalDescription": "사용자를 초대하여 귀하의 조직에 직접 참여하게 하세요.", + "userTypeInternalDescription": "사용자를 초대하여 조직에 직접 참여하게 하세요.", "userTypeExternal": "외부 사용자", "userTypeExternalDescription": "외부 신원 공급자를 사용하여 사용자를 생성하세요.", "accessUserCreateDescription": "새 사용자를 만들기 위한 아래 단계를 따르세요.", @@ -436,6 +438,16 @@ "inviteEmailSent": "사용자에게 초대 이메일 보내기", "inviteValid": "유효 기간", "selectDuration": "지속 시간 선택", + "selectResource": "리소스 선택", + "filterByResource": "리소스별 필터", + "resetFilters": "필터 재설정", + "totalBlocked": "Pangolin으로 차단된 요청", + "totalRequests": "총 요청 수", + "requestsByCountry": "국가별 요청 수", + "requestsByDay": "일자별 요청 수", + "blocked": "차단됨", + "allowed": "허용됨", + "topCountries": "상위 국가", "accessRoleSelect": "역할 선택", "inviteEmailSentDescription": "아래의 접근 링크와 함께 사용자에게 이메일이 전송되었습니다. 사용자는 초대를 수락하기 위해 링크에 접근해야 합니다.", "inviteSentDescription": "사용자가 초대되었습니다. 초대를 수락하려면 아래 링크에 접속해야 합니다.", @@ -464,7 +476,7 @@ "proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.", "proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.", "proxyEnableSSL": "SSL 활성화", - "proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.", + "proxyEnableSSLDescription": "타겟과의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화를 활성화하세요.", "target": "대상", "configureTarget": "대상 구성", "targetErrorFetch": "대상 가져오는 데 실패했습니다.", @@ -480,14 +492,14 @@ "targetsErrorUpdate": "대상 업데이트 실패", "targetsErrorUpdateDescription": "대상 업데이트 중 오류가 발생했습니다.", "targetTlsUpdate": "TLS 설정이 업데이트되었습니다.", - "targetTlsUpdateDescription": "TLS 설정이 성공적으로 업데이트되었습니다.", + "targetTlsUpdateDescription": "TLS 설정이 성공적으로 업데이트되었습니다", "targetErrorTlsUpdate": "TLS 설정 업데이트에 실패했습니다.", "targetErrorTlsUpdateDescription": "TLS 설정을 업데이트하는 동안 오류가 발생했습니다", "proxyUpdated": "프록시 설정이 업데이트되었습니다.", "proxyUpdatedDescription": "프록시 설정이 성공적으로 업데이트되었습니다", "proxyErrorUpdate": "프록시 설정 업데이트에 실패했습니다.", "proxyErrorUpdateDescription": "프록시 설정을 업데이트하는 동안 오류가 발생했습니다", - "targetAddr": "IP / 호스트 이름", + "targetAddr": "호스트", "targetPort": "포트", "targetProtocol": "프로토콜", "targetTlsSettings": "보안 연결 구성", @@ -502,7 +514,7 @@ "targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.", "methodSelect": "선택 방법", "targetSubmit": "대상 추가", - "targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보내려면 대상을 추가하세요.", + "targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보낼 대상을 구성하려면 대상을 추가하세요.", "targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.", "targetsSubmit": "대상 저장", "addTarget": "대상 추가", @@ -516,6 +528,8 @@ "targetCreatedDescription": "대상이 성공적으로 생성되었습니다.", "targetErrorCreate": "대상 생성 실패", "targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.", + "tlsServerName": "TLS 서버 이름", + "tlsServerNameDescription": "SNI를 위한 TLS 서버 이름", "save": "저장", "proxyAdditional": "추가 프록시 설정", "proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성", @@ -607,9 +621,9 @@ "unknownCommand": "알 수 없는 명령", "newtErrorFetchReleases": "릴리스 정보를 가져오는 데 실패했습니다: {err}", "newtErrorFetchLatest": "최신 릴리스를 가져오는 중 오류 발생: {err}", - "newtEndpoint": "Newt 엔드포인트", - "newtId": "뉴트 ID", - "newtSecretKey": "Newt 비밀 키", + "newtEndpoint": "엔드포인트", + "newtId": "ID", + "newtSecretKey": "비밀", "architecture": "아키텍처", "sites": "사이트", "siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "핀코드 설정", "resourcePincodeSetupTitleDescription": "이 리소스를 보호하기 위해 핀 코드를 설정하십시오.", "resourceRoleDescription": "관리자는 항상 이 리소스에 접근할 수 있습니다.", - "resourceUsersRoles": "사용자 및 역할", + "resourceUsersRoles": "접근 제어", "resourceUsersRolesDescription": "이 리소스를 방문할 수 있는 사용자 및 역할을 구성하십시오", "resourceUsersRolesSubmit": "사용자 및 역할 저장", "resourceWhitelistSave": "성공적으로 저장되었습니다.", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "리소스 전송", "siteDestination": "대상 사이트", "searchSites": "사이트 검색", + "countries": "국가", "accessRoleCreate": "역할 생성", "accessRoleCreateDescription": "사용자를 그룹화하고 권한을 관리하기 위해 새 역할을 생성하세요.", "accessRoleCreateSubmit": "역할 생성", @@ -909,6 +924,10 @@ "passwordResetSent": "이 이메일 주소로 비밀번호 재설정 코드를 전송하겠습니다.", "passwordResetCode": "코드 재설정", "passwordResetCodeDescription": "재설정 코드를 확인하려면 이메일을 확인하세요.", + "generatePasswordResetCode": "비밀번호 초기화 코드 생성", + "passwordResetCodeGenerated": "비밀번호 초기화 코드 생성됨", + "passwordResetCodeGeneratedDescription": "이 코드를 사용자와 공유하세요. 사용자는 이를 사용하여 비밀번호를 재설정할 수 있습니다.", + "passwordResetUrl": "리디렉션 URL", "passwordNew": "새 비밀번호", "passwordNewConfirm": "새 비밀번호 확인", "changePassword": "비밀번호 변경", @@ -926,6 +945,9 @@ "pincodeAuth": "인증 코드", "pincodeSubmit2": "코드 제출", "passwordResetSubmit": "재설정 요청", + "passwordResetAlreadyHaveCode": "비밀번호 초기화 코드를 입력하세요", + "passwordResetSmtpRequired": "관리자에게 문의하십시오", + "passwordResetSmtpRequiredDescription": "비밀번호를 재설정하려면 비밀번호 초기화 코드가 필요합니다. 지원을 받으려면 관리자에게 문의하십시오.", "passwordBack": "비밀번호로 돌아가기", "loginBack": "로그인으로 돌아가기", "signup": "가입하기", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "사이트 리소스 목록", "actionUpdateSiteResource": "사이트 리소스 업데이트", "actionListInvitations": "초대 목록", + "actionExportLogs": "로그 내보내기", + "actionViewLogs": "로그 보기", "noneSelected": "선택된 항목 없음", "orgNotFound2": "조직이 없습니다.", "searchProgress": "검색...", "create": "생성", "orgs": "조직", "loginError": "로그인 중 오류가 발생했습니다", + "loginRequiredForDevice": "장치를 인증하려면 로그인이 필요합니다.", "passwordForgot": "비밀번호를 잊으셨나요?", "otpAuth": "이중 인증", "otpAuthDescription": "인증 앱에서 코드를 입력하거나 단일 사용 백업 코드 중 하나를 입력하세요.", @@ -1151,19 +1176,29 @@ "sidebarHome": "홈", "sidebarSites": "사이트", "sidebarResources": "리소스", + "sidebarProxyResources": "공유", + "sidebarClientResources": "비공개", "sidebarAccessControl": "액세스 제어", + "sidebarLogsAndAnalytics": "로그 및 분석", "sidebarUsers": "사용자", + "sidebarAdmin": "관리자", "sidebarInvitations": "초대", "sidebarRoles": "역할", - "sidebarShareableLinks": "공유 가능한 링크", + "sidebarShareableLinks": "링크", "sidebarApiKeys": "API 키", "sidebarSettings": "설정", "sidebarAllUsers": "모든 사용자", "sidebarIdentityProviders": "신원 공급자", "sidebarLicense": "라이선스", "sidebarClients": "클라이언트", + "sidebarUserDevices": "사용자", + "sidebarMachineClients": "기계", "sidebarDomains": "도메인", + "sidebarGeneral": "일반", + "sidebarLogAndAnalytics": "로그 & 통계", "sidebarBluePrints": "청사진", + "sidebarOrganization": "조직", + "sidebarLogsAnalytics": "분석", "blueprints": "청사진", "blueprintsDescription": "선언적 구성을 적용하고 이전 실행을 봅니다", "blueprintAdd": "청사진 추가", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "적용된 청사진의 결과와 발생한 오류를 확인합니다", "blueprintInfo": "청사진 정보", "message": "메시지", - "blueprintContentsDescription": "인프라를 설명하는 YAML 콘텐츠를 정의하십시오", + "blueprintContentsDescription": "인프라를 설명하는 YAML 내용을 정의하십시오", "blueprintErrorCreateDescription": "청사진을 적용하는 중 오류가 발생했습니다", "blueprintErrorCreate": "청사진 생성 오류", "searchBlueprintProgress": "청사진 검색...", @@ -1230,14 +1265,14 @@ "loading": "로딩 중", "restart": "재시작", "domains": "도메인", - "domainsDescription": "조직의 도메인을 관리합니다", + "domainsDescription": "조직에서 사용 가능한 도메인 생성 및 관리", "domainsSearch": "도메인 검색...", "domainAdd": "도메인 추가", "domainAddDescription": "조직에 새로운 도메인을 등록하세요", "domainCreate": "도메인 생성", "domainCreatedDescription": "도메인이 성공적으로 생성되었습니다", "domainDeletedDescription": "도메인이 성공적으로 삭제되었습니다", - "domainQuestionRemove": "계정에서 도메인을 제거하시겠습니까?", + "domainQuestionRemove": "도메인을 제거하시겠습니까?", "domainMessageRemove": "제거되면 도메인이 더 이상 계정과 연관되지 않습니다.", "domainConfirmDelete": "도메인 삭제 확인", "domainDelete": "도메인 삭제", @@ -1285,7 +1320,7 @@ "productUpdateTitle": "제품 업데이트", "productUpdateEmpty": "업데이트 없음", "dismissAll": "모두 해제", - "pangolinUpdateAvailable": "새 버전 사용 가능", + "pangolinUpdateAvailable": "업데이트 가능", "pangolinUpdateAvailableInfo": "버전 {version}을(를) 설치할 준비가 되었습니다", "pangolinUpdateAvailableReleaseNotes": "릴리스 노트 보기", "newtUpdateAvailable": "업데이트 가능", @@ -1430,28 +1465,31 @@ "and": "및", "privacyPolicy": "개인 정보 보호 정책" }, + "signUpMarketing": { + "keepMeInTheLoop": "이메일을 통해 소식, 업데이트 및 새로운 기능을 받아보세요." + }, "siteRequired": "사이트가 필요합니다.", "olmTunnel": "Olm 터널", "olmTunnelDescription": "클라이언트 연결에 Olm 사용", "errorCreatingClient": "클라이언트 생성 오류", "clientDefaultsNotFound": "클라이언트 기본값을 찾을 수 없습니다.", "createClient": "클라이언트 생성", - "createClientDescription": "사이트에 연결하기 위한 새 클라이언트를 생성하십시오.", + "createClientDescription": "개인 리소스에 액세스할 새 클라이언트를 생성하십시오", "seeAllClients": "모든 클라이언트 보기", "clientInformation": "클라이언트 정보", "clientNamePlaceholder": "클라이언트 이름", "address": "주소", "subnetPlaceholder": "서브넷", - "addressDescription": "이 클라이언트가 연결에 사용할 주소", + "addressDescription": "클라이언트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.", "selectSites": "사이트 선택", "sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.", "clientInstallOlm": "Olm 설치", "clientInstallOlmDescription": "시스템에서 Olm을 실행하기", "clientOlmCredentials": "Olm 자격 증명", - "clientOlmCredentialsDescription": "Olm이 서버와 인증하는 방법입니다.", + "clientOlmCredentialsDescription": "이것이 Newt가 서버와 인증하는 방법입니다", "olmEndpoint": "Olm 엔드포인트", - "olmId": "Olm ID", - "olmSecretKey": "Olm 비밀 키", + "olmId": "ID", + "olmSecretKey": "비밀", "clientCredentialsSave": "자격 증명 저장", "clientCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", "generalSettingsDescription": "이 클라이언트에 대한 일반 설정을 구성하세요.", @@ -1463,9 +1501,7 @@ "sitesFetchError": "사이트 가져오는 중 오류가 발생했습니다.", "olmErrorFetchReleases": "Olm 릴리즈 가져오는 중 오류가 발생했습니다.", "olmErrorFetchLatest": "최신 Olm 릴리즈 가져오는 중 오류가 발생했습니다.", - "remoteSubnets": "원격 서브넷", "enterCidrRange": "CIDR 범위 입력", - "remoteSubnetsDescription": "이 사이트에서 원격으로 액세스할 수 있는 CIDR 범위를 추가하세요. 10.0.0.0/24와 같은 형식을 사용하세요. 이는 VPN 클라이언트 연결에만 적용됩니다.", "resourceEnableProxy": "공개 프록시 사용", "resourceEnableProxyDescription": "이 리소스에 대한 공개 프록시를 활성화하십시오. 이를 통해 네트워크 외부로부터 클라우드를 통해 열린 포트에서 리소스에 액세스할 수 있습니다. Traefik 구성이 필요합니다.", "externalProxyEnabled": "외부 프록시 활성화됨", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "이 대상을 모니터링하여 건강 상태를 확인하세요. 필요에 따라 대상과 다른 엔드포인트를 모니터링할 수 있습니다.", "healthScheme": "방법", "healthSelectScheme": "방법 선택", + "healthCheckPortInvalid": "올바르지 않은 서브넷 마스크입니다. 1에서 65535 사이여야 합니다", "healthCheckPath": "경로", "healthHostname": "IP / 호스트", "healthPort": "포트", "healthCheckPathDescription": "상태 확인을 위한 경로입니다.", - "healthyIntervalSeconds": "정상 간격", - "unhealthyIntervalSeconds": "비정상 간격", + "healthyIntervalSeconds": "정상 간격(초)", + "unhealthyIntervalSeconds": "비정상 간격(초)", "IntervalSeconds": "정상 간격", - "timeoutSeconds": "시간 초과", + "timeoutSeconds": "타임아웃(초)", "timeIsInSeconds": "시간은 초 단위입니다", "retryAttempts": "재시도 횟수", "expectedResponseCodes": "예상 응답 코드", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "도메인 수정", "siteName": "사이트 이름", "proxyPort": "포트", - "resourcesTableProxyResources": "프록시 리소스", - "resourcesTableClientResources": "클라이언트 리소스", + "resourcesTableProxyResources": "공유", + "resourcesTableClientResources": "비공개", "resourcesTableNoProxyResourcesFound": "프록시 리소스를 찾을 수 없습니다.", "resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.", "resourcesTableDestination": "대상지", - "resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.", + "resourcesTableAlias": "별칭", "resourcesTableClients": "클라이언트", "resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.", "resourcesTableNoTargets": "대상 없음", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "오프라인", "resourcesTableUnknown": "알 수 없음", "resourcesTableNotMonitored": "모니터링되지 않음", - "editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정", - "editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.", + "editInternalResourceDialogEditClientResource": "비공개 리소스 수정", + "editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요", "editInternalResourceDialogResourceProperties": "리소스 속성", "editInternalResourceDialogName": "이름", "editInternalResourceDialogProtocol": "프로토콜", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식", "editInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.", "editInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.", + "editInternalResourceDialogPortModeRequired": "포트 모드에는 프로토콜, 프록시 포트 및 대상 포트가 필요합니다", + "editInternalResourceDialogMode": "모드", + "editInternalResourceDialogModePort": "포트", + "editInternalResourceDialogModeHost": "호스트", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "대상지", + "editInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.", + "editInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 또는 호스트 네임 주소입니다.", + "editInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.", + "editInternalResourceDialogAlias": "별칭", + "editInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.", "createInternalResourceDialogNoSitesAvailable": "사용 가능한 사이트가 없습니다.", "createInternalResourceDialogNoSitesAvailableDescription": "내부 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.", "createInternalResourceDialogClose": "닫기", - "createInternalResourceDialogCreateClientResource": "클라이언트 리소스 생성", - "createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다.", + "createInternalResourceDialogCreateClientResource": "사이트 리소스 생성", + "createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다", "createInternalResourceDialogResourceProperties": "리소스 속성", "createInternalResourceDialogName": "이름", "createInternalResourceDialogSite": "사이트", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식", "createInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.", "createInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.", + "createInternalResourceDialogPortModeRequired": "포트 모드에는 프로토콜, 프록시 포트 및 대상 포트가 필요합니다", + "createInternalResourceDialogMode": "모드", + "createInternalResourceDialogModePort": "포트", + "createInternalResourceDialogModeHost": "호스트", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "대상지", + "createInternalResourceDialogDestinationHostDescription": "사이트 네트워크의 자원 IP 주소입니다.", + "createInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.", + "createInternalResourceDialogAlias": "별칭", + "createInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.", "siteConfiguration": "설정", "siteAcceptClientConnections": "클라이언트 연결 허용", - "siteAcceptClientConnectionsDescription": "이 Newt 인스턴스를 게이트웨이로 사용하여 다른 장치가 연결될 수 있도록 허용합니다.", - "siteAddress": "사이트 주소", - "siteAddressDescription": "클라이언트가 연결하기 위한 호스트의 IP 주소를 지정합니다. 이는 클라이언트가 주소를 지정하기 위한 Pangolin 네트워크의 사이트 내부 주소입니다. 조직 서브넷 내에 있어야 합니다.", + "siteAcceptClientConnectionsDescription": "사용자 장치와 클라이언트가 이 사이트의 리소스에 접근할 수 있도록 허용하세요. 나중에 변경할 수 있습니다.", + "siteAddress": "사이트 주소(고급)", + "siteAddressDescription": "사이트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.", + "siteNameDescription": "나중에 변경할 수 있는 사이트의 표시 이름입니다.", "autoLoginExternalIdp": "외부 IDP로 자동 로그인", "autoLoginExternalIdpDescription": "인증을 위해 외부 IDP로 사용자를 즉시 리디렉션합니다.", "selectIdp": "IDP 선택", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.", "autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.", "remoteExitNodeManageRemoteExitNodes": "원격 노드", - "remoteExitNodeDescription": "네트워크 연결성을 확장하고 클라우드 의존도를 줄이기 위해 하나 이상의 원격 노드를 자체 호스트하십시오.", + "remoteExitNodeDescription": "하나 이상의 원격 노드를 자체 호스팅하여 네트워크 연결을 확장하고 클라우드에 대한 의존도를 줄입니다.", "remoteExitNodes": "노드", "searchRemoteExitNodes": "노드 검색...", "remoteExitNodeAdd": "노드 추가", @@ -1730,7 +1789,7 @@ "idpAzureConfiguration": "Azure Entra ID 구성", "idpAzureConfigurationDescription": "Azure Entra ID OAuth2 자격 증명을 구성합니다.", "idpTenantId": "테넌트 ID", - "idpTenantIdPlaceholder": "your-tenant-id", + "idpTenantIdPlaceholder": "테넌트 ID", "idpAzureTenantIdDescription": "Azure 액티브 디렉터리 개요에서 찾은 Azure 테넌트 ID", "idpAzureClientIdDescription": "Azure 앱 등록 클라이언트 ID", "idpAzureClientSecretDescription": "Azure 앱 등록 클라이언트 비밀", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "이중 인증 활성화", "completeSecuritySteps": "보안 단계 완료", "securitySettings": "보안 설정", - "securitySettingsDescription": "조직에 대한 보안 정책을 구성합니다", + "securitySettingsDescription": "조직의 보안 정책을 구성하세요", "requireTwoFactorForAllUsers": "모든 사용자에 대해 이중 인증 요구", "requireTwoFactorDescription": "활성화되면, 이 조직의 모든 내부 사용자는 조직에 접근하기 위해 이중 인증을 활성화해야 합니다.", "requireTwoFactorDisabledDescription": "이 기능을 사용하려면 유효한 라이선스(Enterprise) 또는 활성 구독(SaaS)가 필요합니다.", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "엔터프라이즈 에디션", "unlicensed": "라이선스 없음", "beta": "베타", - "manageClients": "클라이언트 관리", - "manageClientsDescription": "클라이언트는 당신의 사이트에 연결할 수 있는 디바이스입니다.", + "manageUserDevices": "사용자 초대를 제어", + "manageUserDevicesDescription": "리소스에 개인적으로 연결하기 위해 사용자가 사용하는 장치를 보고 관리하세요", + "manageMachineClients": "기계 클라이언트 관리", + "manageMachineClientsDescription": "서버와 시스템이 리소스에 개인적으로 연결하는 데 사용하는 클라이언트를 생성하고 관리하십시오", + "clientsTableUserClients": "사용자", + "clientsTableMachineClients": "기계", "licenseTableValidUntil": "유효 기한", "saasLicenseKeysSettingsTitle": "엔터프라이즈 라이선스", "saasLicenseKeysSettingsDescription": "자체 호스팅된 Pangolin 인스턴스를 위한 엔터프라이즈 라이선스 키를 생성하고 관리합니다.", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "제거", "sidebarEnableEnterpriseLicense": "엔터프라이즈 라이선스 활성화", "cannotbeUndone": "이 작업은 되돌릴 수 없습니다.", - "toConfirm": "확인하려면", + "toConfirm": "확인을 위해.", "deleteClientQuestion": "고객을 사이트와 조직에서 제거하시겠습니까?", "clientMessageRemove": "제거되면 클라이언트는 사이트에 더 이상 연결할 수 없습니다.", "sidebarLogs": "로그", "request": "요청", + "requests": "요청", "logs": "로그", "logsSettingsDescription": "이 조직에서 수집된 로그를 모니터링합니다", "searchLogs": "로그 검색...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "이유", "requestLogs": "요청 로그", + "requestAnalytics": "요청 분석", "host": "호스트", "location": "위치", "actionLogs": "작업 로그", @@ -2029,6 +2094,7 @@ "logRetention": "로그 보관", "logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다", "requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다", + "requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기", "logRetentionRequestLabel": "요청 로그 보관", "logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지", "logRetentionAccessLabel": "접근 로그 보관", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 일", "logRetention90Days": "90 일", "logRetentionForever": "영구", + "logRetentionEndOfFollowingYear": "다음 연도 말", "actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다", "accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다", "licenseRequiredToUse": "이 기능을 사용하려면 Enterprise 라이선스가 필요합니다.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "와일드카드 인증서 선호", "unverified": "검증되지 않음", "domainSetting": "도메인 설정", - "domainSettingDescription": "도메인에 대한 설정을 구성하세요.", + "domainSettingDescription": "도메인 설정 구성", "preferWildcardCertDescription": "와일드카드 인증서를 생성하려고 시도합니다 (올바르게 구성된 인증서 해결사가 필요합니다).", "recordName": "레코드 이름", "auto": "자동", @@ -2066,9 +2133,9 @@ "olmUpdateAvailableInfo": "올름의 새 버전이 이용 가능합니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.", "client": "클라이언트", "proxyProtocol": "프록시 프로토콜 설정", - "proxyProtocolDescription": "프록시 프로토콜을 구성하여 TCP/UDP 서비스에 대한 클라이언트 IP 주소를 보존하십시오.", + "proxyProtocolDescription": "TCP 서비스에 대한 클라이언트 IP 주소를 유지하도록 프록시 프로토콜을 구성하세요.", "enableProxyProtocol": "프록시 프로토콜 활성화", - "proxyProtocolInfo": "TCP/UDP 백엔드의 클라이언트 IP 주소를 보존합니다", + "proxyProtocolInfo": "TCP 백엔드에 대한 클라이언트 IP 주소를 유지합니다.", "proxyProtocolVersion": "프록시 프로토콜 버전", "version1": " 버전 1 (추천)", "version2": "버전 2", @@ -2097,6 +2164,43 @@ "supportMessageSent": "메시지 전송 완료!", "supportWillContact": "곧 연락드리겠습니다!", "selectLogRetention": "로그 보존 선택", + "terms": "약관", + "privacy": "개인정보 보호", + "security": "보안", + "docs": "문서", + "deviceActivation": "장치 활성화", + "deviceCodeInvalidFormat": "코드는 9자리여야 합니다 (예: A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "무효하거나 만료된 코드", + "deviceCodeVerifyFailed": "이메일 확인에 실패했습니다:", + "signedInAs": "로그인한 사용자", + "deviceCodeEnterPrompt": "기기에 표시된 코드를 입력하세요", + "continue": "계속 진행하기", + "deviceUnknownLocation": "알 수 없는 위치", + "deviceAuthorizationRequested": "이 인증 요청은 {location}에서 {date}에 요청되었습니다. 이 장치에 액세스 권한을 부여할 신뢰할 수 있는 경우 확인하세요.", + "deviceLabel": "장치: {deviceName}", + "deviceWantsAccess": "계정에 액세스하려고 합니다", + "deviceExistingAccess": "기존 액세스:", + "deviceFullAccess": "계정에 대한 전체 액세스", + "deviceOrganizationsAccess": "계정이 접근할 수 있는 모든 조직에 대한 접근", + "deviceAuthorize": "{applicationName} 권한 부여", + "deviceConnected": "장치가 연결되었습니다!", + "deviceAuthorizedMessage": "장치가 계정에 액세스할 수 있도록 승인되었습니다.", + "pangolinCloud": "판골린 클라우드", + "viewDevices": "장치 보기", + "viewDevicesDescription": "연결된 장치를 관리하십시오", + "noDevices": "장치를 찾을 수 없습니다", + "dateCreated": "생성 날짜", + "unnamedDevice": "이름 없는 장치", + "deviceQuestionRemove": "이 장치를 삭제하시겠습니까?", + "deviceMessageRemove": "이 작업은 취소할 수 없습니다.", + "deviceDeleteConfirm": "장치 삭제", + "deleteDevice": "장치 삭제", + "errorLoadingDevices": "장치 로딩 오류", + "failedToLoadDevices": "장치를 로드하지 못했습니다", + "deviceDeleted": "장치 삭제 완료", + "deviceDeletedDescription": "장치가 성공적으로 삭제되었습니다.", + "errorDeletingDevice": "장치 삭제 오류", + "failedToDeleteDevice": "장치를 삭제하지 못했습니다", "showColumns": "열 표시", "hideColumns": "열 숨기기", "columnVisibility": "열 가시성", @@ -2111,10 +2215,14 @@ "enableSelected": "선택된 항목 활성화", "disableSelected": "선택된 항목 비활성화", "checkSelectedStatus": "선택된 항목 상태 확인", + "clients": "클라이언트", + "accessClientSelect": "기계 클라이언트 선택", + "resourceClientDescription": "관리자는 항상 이 리소스에 접근할 수 있습니다", + "regenerate": "재생성", "credentials": "자격 증명", "savecredentials": "자격 증명 저장", - "regeneratecredentials": "재생성", - "regenerateCredentials": "자격 증명을 재생성하고 저장합니다", + "regenerateCredentialsButton": "자격 증명 다시 생성", + "regenerateCredentials": "자격 증명 다시 생성", "generatedcredentials": "생성된 자격 증명", "copyandsavethesecredentials": "이 자격 증명을 복사하여 저장합니다", "copyandsavethesecredentialsdescription": "이 페이지를 떠난 후에는 자격 증명이 다시 표시되지 않습니다. 지금 안전하게 저장하십시오.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "자격 증명이 성공적으로 재생성 및 저장되었습니다.", "credentialsSaveError": "자격 증명 저장 오류", "credentialsSaveErrorDescription": "자격 증명을 재생성하고 저장하는 동안 오류가 발생했습니다.", - "regenerateCredentialsWarning": "자격 증명을 재생성하면 이전 자격 증명이 무효화됩니다. 이 자격 증명을 사용하는 모든 구성을 업데이트하십시오.", + "regenerateCredentialsWarning": "자격 증명을 다시 생성하면 이전 것들이 무효화되면서 연결이 끊어집니다. 이러한 자격 증명을 사용하는 모든 구성을 업데이트하세요.", "confirm": "확인", "regenerateCredentialsConfirmation": "자격 증명을 재생성하시겠습니까?", "endpoint": "엔드포인트", "Id": "아이디", "SecretKey": "비밀 키", - "featureDisabledTooltip": "이 기능은 엔터프라이즈 플랜에서만 사용할 수 있으며 라이센스가 필요합니다.", "niceId": "예쁜 ID", "niceIdUpdated": "예쁜 ID 업데이트됨", "niceIdUpdatedSuccessfully": "예쁜 ID가 성공적으로 업데이트되었습니다", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "예쁜 ID를 업데이트하는 동안 오류가 발생했습니다.", "niceIdCannotBeEmpty": "예쁜 ID는 비워둘 수 없습니다", "enterIdentifier": "식별자 입력", - "identifier": "식별자" + "identifier": "식별자", + "deviceLoginUseDifferentAccount": "본인이 아닙니까? 다른 계정을 사용하세요.", + "deviceLoginDeviceRequestingAccessToAccount": "장치가 이 계정에 접근하려고 합니다.", + "noData": "데이터 없음", + "machineClients": "기계 클라이언트", + "install": "설치", + "run": "실행", + "clientNameDescription": "나중에 변경할 수 있는 클라이언트의 표시 이름입니다.", + "clientAddress": "클라이언트 주소(고급)", + "setupFailedToFetchSubnet": "기본값 로드 실패", + "setupSubnetAdvanced": "서브넷(고급)", + "setupSubnetDescription": "이 조직의 네트워크 구성에 대한 서브넷입니다.", + "siteRegenerateAndDisconnect": "재생성 및 연결 해제", + "siteRegenerateAndDisconnectConfirmation": "자격 증명을 재생성하고 이 사이트와의 연결을 해제하시겠습니까?", + "siteRegenerateAndDisconnectWarning": "이 과정은 자격 증명을 다시 생성하고 사이트와의 연결을 즉시 해제합니다. 사이트는 새 자격 증명으로 다시 시작되어야 합니다.", + "siteRegenerateCredentialsConfirmation": "이 사이트에 대한 자격 증명을 다시 생성하시겠습니까?", + "siteRegenerateCredentialsWarning": "이 과정은 자격 증명을 다시 생성합니다. 수동으로 다시 시작하고 새 자격 증명을 사용하기 전까지 사이트는 연결된 상태로 유지됩니다.", + "clientRegenerateAndDisconnect": "재생성 및 연결 해제", + "clientRegenerateAndDisconnectConfirmation": "자격 증명을 재생성하고 이 클라이언트와의 연결을 해제하시겠습니까?", + "clientRegenerateAndDisconnectWarning": "이 과정은 자격 증명을 다시 생성하고 클라이언트와의 연결을 즉시 해제합니다. 클라이언트는 새 자격 증명으로 다시 시작되어야 합니다.", + "clientRegenerateCredentialsConfirmation": "이 클라이언트에 대한 자격 증명을 다시 생성하시겠습니까?", + "clientRegenerateCredentialsWarning": "이 과정은 자격 증명을 다시 생성합니다. 수동으로 다시 시작하고 새 자격 증명을 사용하기 전까지 클라이언트는 연결된 상태로 유지됩니다.", + "remoteExitNodeRegenerateAndDisconnect": "재생성 및 연결 해제", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "자격 증명을 재생성하고 이 원격 종료 노드와의 연결을 해제하시겠습니까?", + "remoteExitNodeRegenerateAndDisconnectWarning": "이 과정은 자격 증명을 다시 생성하고 원격 종료 노드와의 연결을 즉시 해제합니다. 원격 종료 노드는 새 자격 증명으로 다시 시작되어야 합니다.", + "remoteExitNodeRegenerateCredentialsConfirmation": "이 원격 종료 노드에 대한 자격 증명을 다시 생성하시겠습니까?", + "remoteExitNodeRegenerateCredentialsWarning": "이 과정은 자격 증명을 다시 생성합니다. 수동으로 다시 시작하고 새 자격 증명을 사용하기 전까지 원격 종료 노드는 연결된 상태로 유지됩니다.", + "agent": "에이전트" } diff --git a/messages/nb-NO.json b/messages/nb-NO.json index c2f8f791..ec7553b6 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -1,12 +1,12 @@ { - "setupCreate": "Lag din organisasjon, område og dine ressurser", + "setupCreate": "Opprett organisasjonen, nettstedet og ressursene", "setupNewOrg": "Ny Organisasjon", "setupCreateOrg": "Opprett organisasjon", "setupCreateResources": "Opprett ressurser", "setupOrgName": "Organisasjonsnavn", - "orgDisplayName": "Dette er visningsnavnet til organisasjonen din.", + "orgDisplayName": "Dette er organisasjonens visningsnavn.", "orgId": "Organisasjons-ID", - "setupIdentifierMessage": "Dette er den unike identifikator for din organisasjon. Dette er separat fra visningsnavnet.", + "setupIdentifierMessage": "Dette er den unike identifikatoren for organisasjonen.", "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.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Når nettstedet er fjernet, vil det ikke lenger være tilgjengelig. Alle målene for nettstedet vil også bli fjernet.", "siteQuestionRemove": "Er du sikker på at du vil fjerne nettstedet fra organisasjonen?", "siteManageSites": "Administrer Områder", - "siteDescription": "Tillat tilkobling til nettverket ditt gjennom sikre tunneler", + "siteDescription": "Opprette og administrere nettsteder for å aktivere tilkobling til private nettverk", "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", + "siteCreateDescription": "Opprett et nytt nettsted for å koble til ressurser", "close": "Lukk", "siteErrorCreate": "Feil ved oppretting av område", "siteErrorCreateKeyPair": "Nøkkelpar eller standardinnstillinger for område ikke funnet", @@ -74,7 +74,7 @@ "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", + "WgConfigurationDescription": "Bruk følgende konfigurasjon for å koble til nettverket", "operatingSystem": "Operativsystem", "commands": "Kommandoer", "recommended": "Anbefalt", @@ -87,32 +87,32 @@ "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", + "siteSettingDescription": "Konfigurere innstillingene på nettstedet", "siteSetting": "{siteName} Innstillinger", - "siteNewtTunnel": "Newt Tunnel (Anbefalt)", - "siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.", + "siteNewtTunnel": "Nyhetsnettsted (anbefalt)", + "siteNewtTunnelDescription": "Lekkeste måte å lage et inngangspunkt til ethvert nettverk. Ingen ekstra oppsett på.", "siteWg": "Grunnleggende WireGuard", "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", "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": "Lokale ressurser. Ingen tunnelering. Bare tilgjengelig på eksterne noder.", "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", + "siteTunnelDescription": "Avgjør hvordan du vil koble deg til nettstedet", + "siteNewtCredentials": "Legitimasjon", + "siteNewtCredentialsDescription": "Dette er hvordan nettstedet vil godkjenne med serveren", + "siteCredentialsSave": "Lagre brukeropplysninger", "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", + "shareDescription": "Opprett delbare lenker for å gi midlertidige eller permanent tilgang til proxyressurser", "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.", + "shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.", "accessToken": "Tilgangsnøkkel", "usageExamples": "Brukseksempler", "tokenId": "Token-ID", @@ -121,7 +121,7 @@ "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.", + "shareTokenSecurety": "Hold tilgangstoken sikker. Ikke del den i offentlig tilgjengelige områder eller klientsidekode.", "shareErrorFetchResource": "Klarte ikke å hente ressurser", "shareErrorFetchResourceDescription": "En feil oppstod under henting av ressursene", "shareErrorCreate": "Mislyktes med å opprette delingslenke", @@ -144,8 +144,10 @@ "expires": "Utløper", "never": "Aldri", "shareErrorSelectResource": "Vennligst velg en ressurs", - "resourceTitle": "Administrer Ressurser", - "resourceDescription": "Opprett sikre proxyer til dine private applikasjoner", + "proxyResourceTitle": "Administrere offentlige ressurser", + "proxyResourceDescription": "Opprett og administrer ressurser som er offentlig tilgjengelige via en nettleser", + "clientResourceTitle": "Administrer private ressurser", + "clientResourceDescription": "Opprette og administrere ressurser som bare er tilgjengelige via en tilkoblet klient", "resourcesSearch": "Søk i ressurser...", "resourceAdd": "Legg til ressurs", "resourceErrorDelte": "Feil ved sletting av ressurs", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Når den er fjernet, vil ressursen ikke lenger være tilgjengelig. Alle mål knyttet til ressursen vil også bli fjernet.", "resourceQuestionRemove": "Er du sikker på at du vil fjerne ressursen fra organisasjonen?", "resourceHTTP": "HTTPS-ressurs", - "resourceHTTPDescription": "Proxy-forespørsler til appen din over HTTPS ved bruk av et underdomene eller grunndomene.", + "resourceHTTPDescription": "Proxy-forespørsler til appen over HTTPS ved hjelp av et underdomene eller basisdomene.", "resourceRaw": "Rå TCP/UDP-ressurs", - "resourceRawDescription": "Proxyer forespørsler til appen din over TCP/UDP ved å bruke et portnummer.", + "resourceRawDescription": "Proxy ber om til appen over TCP/UDP med et portnummer. Dette fungerer bare når nettsteder er koblet til noder.", "resourceCreate": "Opprett ressurs", "resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs", "resourceSeeAll": "Se alle ressurser", @@ -171,22 +173,22 @@ "noCountryFound": "Ingen land funnet.", "siteSelectionDescription": "Dette området vil gi tilkobling til mål.", "resourceType": "Ressurstype", - "resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din", + "resourceTypeDescription": "Bestemme hvordan denne ressursen skal brukes", "resourceHTTPSSettings": "HTTPS-innstillinger", - "resourceHTTPSSettingsDescription": "Konfigurer tilgang til ressursen din over HTTPS", + "resourceHTTPSSettingsDescription": "Konfigurer hvordan ressursen skal nås over HTTPS", "domainType": "Domenetype", "subdomain": "Underdomene", "baseDomain": "Grunndomene", - "subdomnainDescription": "Underdomenet der ressursen din vil være tilgjengelig.", + "subdomnainDescription": "Underdomenet hvor ressursen vil være tilgjengelig.", "resourceRawSettings": "TCP/UDP-innstillinger", - "resourceRawSettingsDescription": "Konfigurer hvordan din ressurs vil bli tilgjengelig over TCP/UDP. Du kartlegger ressursen til en port på vertsserveren Pangolin slik at du får tilgang til ressursene fra server-ip:mappet port.", + "resourceRawSettingsDescription": "Konfigurer hvordan ressursen vil bli tilgjengelig 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", + "resourceConfigDescription": "Kopier og lim inn disse konfigurasjons-øyeblikkene for å sette opp TCP/UDP ressursen", "resourceAddEntrypoints": "Traefik: Legg til inngangspunkter", "resourceExposePorts": "Gerbil: Eksponer Porter i Docker Compose", "resourceLearnRaw": "Lær hvordan å konfigurere TCP/UDP-ressurser", @@ -202,14 +204,14 @@ "proxy": "Proxy", "internal": "Intern", "rules": "Regler", - "resourceSettingDescription": "Konfigurer innstillingene på ressursen din", + "resourceSettingDescription": "Konfigurere innstillingene på ressursen", "resourceSetting": "{resourceName} Innstillinger", - "alwaysAllow": "Alltid tillat", - "alwaysDeny": "Alltid avslå", + "alwaysAllow": "Omgå Auth", + "alwaysDeny": "Blokker tilgang", "passToAuth": "Pass til Autentisering", - "orgSettingsDescription": "Konfigurer organisasjonens generelle innstillinger", + "orgSettingsDescription": "Konfigurere organisasjonens innstillinger", "orgGeneralSettings": "Organisasjonsinnstillinger", - "orgGeneralSettingsDescription": "Administrer dine organisasjonsdetaljer og konfigurasjon", + "orgGeneralSettingsDescription": "Behandle organisasjonens detaljer og konfigurasjon", "saveGeneralSettings": "Lagre generelle innstillinger", "saveSettings": "Lagre innstillinger", "orgDangerZone": "Faresone", @@ -232,7 +234,7 @@ "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", + "accessUsersDescription": "Inviter og behandle brukere med tilgang til denne organisasjonen", "accessUsersSearch": "Søk etter brukere...", "accessUserCreate": "Opprett bruker", "accessUserRemove": "Fjern bruker", @@ -241,13 +243,13 @@ "role": "Rolle", "nameRequired": "Navn er påkrevd", "accessRolesManage": "Administrer Roller", - "accessRolesDescription": "Konfigurer roller for å administrere tilgang til organisasjonen din", + "accessRolesDescription": "Opprett og administrer roller for brukere i organisasjonen", "accessRolesSearch": "Søk etter roller...", "accessRolesAdd": "Legg til rolle", "accessRoleDelete": "Slett rolle", "description": "Beskrivelse", "inviteTitle": "Åpne invitasjoner", - "inviteDescription": "Administrer invitasjonene dine til andre brukere", + "inviteDescription": "Administrer invitasjoner til andre brukere for å bli med i organisasjonen", "inviteSearch": "Søk i invitasjoner...", "minutes": "Minutter", "hours": "Timer", @@ -261,13 +263,13 @@ "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", + "apiKeysCreateDescription": "Generer en ny API-nøkkel for organisasjonen", "apiKeysGeneralSettings": "Tillatelser", "apiKeysGeneralSettingsDescription": "Finn ut hva denne API-nøkkelen kan gjøre", - "apiKeysList": "Din API-nøkkel", - "apiKeysSave": "Lagre API-nøkkelen din", + "apiKeysList": "Ny API-nøkkel", + "apiKeysSave": "Lagre API-nøkkel", "apiKeysSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", - "apiKeysInfo": "Din API-nøkkel er:", + "apiKeysInfo": "API-nøkkelen er:", "apiKeysConfirmCopy": "Jeg har kopiert API-nøkkelen", "generate": "Generer", "done": "Ferdig", @@ -424,7 +426,7 @@ "userCreated": "Bruker opprettet", "userCreatedDescription": "Brukeren har blitt vellykket opprettet.", "userTypeInternal": "Intern bruker", - "userTypeInternalDescription": "Inviter en bruker til å bli med i organisasjonen din direkte.", + "userTypeInternalDescription": "Inviter en bruker til å bli med direkte på organisasjonen.", "userTypeExternal": "Ekstern bruker", "userTypeExternalDescription": "Opprett en bruker med en ekstern identitetsleverandør.", "accessUserCreateDescription": "Følg stegene under for å opprette en ny bruker", @@ -436,6 +438,16 @@ "inviteEmailSent": "Send invitasjonsepost til bruker", "inviteValid": "Gyldig for", "selectDuration": "Velg varighet", + "selectResource": "Velg ressurs", + "filterByResource": "Filtrer etter ressurser", + "resetFilters": "Tilbakestill filtre", + "totalBlocked": "Forespørsler blokkert av Pangolin", + "totalRequests": "Totalt antall forespørsler", + "requestsByCountry": "Forespørsler fra land", + "requestsByDay": "Forespørsler per dag", + "blocked": "Blokkert", + "allowed": "Tillatt", + "topCountries": "Flest land", "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.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Lagre tilgangskontroller", "roles": "Roller", "accessUsersRoles": "Administrer brukere og roller", - "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen din.", + "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen", "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", - "proxyEnableSSLDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til dine mål.", + "proxyEnableSSLDescription": "Aktivere SSL/TLS-kryptering for sikker HTTPS tilkobling til målene.", "target": "Target", "configureTarget": "Konfigurer mål", "targetErrorFetch": "Kunne ikke hente mål", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Feilet å oppdatere mål", "targetsErrorUpdateDescription": "En feil oppsto under oppdatering av mål", "targetTlsUpdate": "TLS-innstillinger oppdatert", - "targetTlsUpdateDescription": "Dine TLS-innstillinger er oppdatert", + "targetTlsUpdateDescription": "TLS-innstillinger har blitt 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", + "proxyUpdatedDescription": "Proxy innstillinger har blitt oppdatert", "proxyErrorUpdate": "En feil oppsto under oppdatering av proxyinnstillinger", "proxyErrorUpdateDescription": "En feil oppsto under oppdatering av proxyinnstillinger", - "targetAddr": "IP / vertsnavn", + "targetAddr": "Vert", "targetPort": "Port", "targetProtocol": "Protokoll", "targetTlsSettings": "Sikker tilkoblings-konfigurasjon", - "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din", + "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen", "targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger", "targetTlsSni": "TLS servernavn", "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 dine backend-tjenester", + "targetsDescription": "Sett opp mål for rutetrafikk til backend tjenestene", "targetStickySessions": "Aktiver klebrige sesjoner", "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", "methodSelect": "Velg metode", "targetSubmit": "Legg til mål", - "targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til din backend.", + "targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til backend.", "targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.", "targetsSubmit": "Lagre mål", "addTarget": "Legg til mål", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Målet har blitt opprettet", "targetErrorCreate": "Kunne ikke opprette målet", "targetErrorCreateDescription": "Det oppstod en feil under oppretting av målet", + "tlsServerName": "TLS servernavn", + "tlsServerNameDescription": "Tjenernavnet som skal brukes for SNI", "save": "Lagre", "proxyAdditional": "Ytterligere Proxy-innstillinger", - "proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger", + "proxyAdditionalDescription": "Konfigurer hvordan ressursen håndterer proxy-innstillingene", "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", @@ -558,7 +572,7 @@ "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.", + "rulesAboutDescription": "Regler gir mulighet til å kontrollere tilgangen til ressursen basert på et sett av kriterier. Du kan opprette regler for å tillate eller nekte tilgang basert på IP-adresse eller URL-bane.", "rulesActions": "Handlinger", "rulesActionAlwaysAllow": "Alltid Tillat: Omgå alle autentiserings metoder", "rulesActionAlwaysDeny": "Alltid Nekt: Blokker alle forespørsler; ingen autentisering kan forsøkes", @@ -570,7 +584,7 @@ "rulesEnable": "Aktiver regler", "rulesEnableDescription": "Aktiver eller deaktiver regelvurdering for denne ressursen", "rulesResource": "Konfigurasjon av ressursregler", - "rulesResourceDescription": "Konfigurere regler for tilgangskontroll til ressursen din", + "rulesResourceDescription": "Konfigurer regler for å kontrollere tilgang til ressursen", "ruleSubmit": "Legg til regel", "rulesNoOne": "Ingen regler. Legg til en regel ved å bruke skjemaet.", "rulesOrder": "Regler evalueres etter prioritet i stigende rekkefølge.", @@ -586,7 +600,7 @@ "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.", + "resourcesDescription": "Ressurser er proxyer til applikasjoner som kjører i det private nettverket. Opprett en ressurs for enhver HTTP/HTTPS eller rå TCP/UDP tjeneste på ditt private nettverk. Hver ressurs må kobles til et nettsted for å aktivere privat, sikker tilkobling gjennom en kryptert WireGuard tunnel.", "resourcesWireGuardConnect": "Sikker tilkobling med WireGuard-kryptering", "resourcesMultipleAuthenticationMethods": "Konfigurer flere autentiseringsmetoder", "resourcesUsersRolesAccess": "Bruker- og rollebasert tilgangskontroll", @@ -597,7 +611,7 @@ "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.", + "shareDescription2": "Opprett delbare lenker til ressurser. Lenker gir midlertidig eller ubegrenset tilgang til din ressurs. Du kan konfigurere utløpsvarigheten på lenken når du oppretter en.", "shareEasyCreate": "Enkelt å lage og dele", "shareConfigurableExpirationDuration": "Konfigurerbar utløpsvarighet", "shareSecureAndRevocable": "Sikker og tilbakekallbar", @@ -607,19 +621,19 @@ "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", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Sikkerhetsnø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.", + "siteWgAnyClients": "Bruk hvilken som helst WireGuard klient til å koble til. Du må adressere interne ressurser ved hjelp av peer IP.", "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", + "accessRoleSelect2": "Velg roller", + "accessUserSelect": "Velg brukere", "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.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Angi PIN-kode", "resourcePincodeSetupTitleDescription": "Sett en pinkode for å beskytte denne ressursen", "resourceRoleDescription": "Administratorer har alltid tilgang til denne ressursen.", - "resourceUsersRoles": "Brukere og Roller", + "resourceUsersRoles": "Tilgangskontroller", "resourceUsersRolesDescription": "Konfigurer hvilke brukere og roller som har tilgang til denne ressursen", "resourceUsersRolesSubmit": "Lagre brukere og roller", "resourceWhitelistSave": "Lagring vellykket", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Overfør ressurs", "siteDestination": "Destinasjonsområde", "searchSites": "Søk områder", + "countries": "Land", "accessRoleCreate": "Opprett rolle", "accessRoleCreateDescription": "Opprett en ny rolle for å gruppere brukere og administrere deres tillatelser.", "accessRoleCreateSubmit": "Opprett rolle", @@ -766,15 +781,15 @@ "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", + "idpClientIdDescription": "OAuth2 klient-ID fra identitet leverandøren", "idpClientSecret": "Klienthemmelighet", - "idpClientSecretDescription": "OAuth2-klienthemmeligheten fra din identitetsleverandør", + "idpClientSecretDescription": "Klient-hemmeligheten med OAuth2 fra identitet leverandøren", "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.", + "idpOidcConfigureAlertDescription": "Etter at du har opprettet identitetsleverandøren, må du konfigurere callback-URLen i identitetsleverandørens innstillinger. Tilbakeringings URL vil bli lagt til etter vellykket oppretting.", "idpToken": "Token-konfigurasjon", "idpTokenDescription": "Konfigurer hvordan brukerinformasjon trekkes ut fra ID-tokenet", "idpJmespathAbout": "Om JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Opprett identitetsleverandør", "orgPolicies": "Organisasjonsretningslinjer", "idpSettings": "{idpName} Innstillinger", - "idpCreateSettingsDescription": "Konfigurer innstillingene for din identitetsleverandør", + "idpCreateSettingsDescription": "Konfigurer innstillingene for identiteten leverandøren", "roleMapping": "Rolletilordning", "orgMapping": "Organisasjon Kartlegging", "orgPoliciesSearch": "Søk i organisasjonens retningslinjer...", @@ -826,7 +841,7 @@ "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.", + "redirectUrlAboutDescription": "Dette er URLen som brukere vil bli omdirigert etter autentisering. Du må konfigurere denne URLen i identitetsleverandørens innstillinger.", "pangolinAuth": "Autentisering - Pangolin", "verificationCodeLengthRequirements": "Din verifiseringskode må være 8 tegn.", "errorOccurred": "Det oppstod en feil", @@ -909,6 +924,10 @@ "passwordResetSent": "Vi sender en kode for tilbakestilling av passord til denne e-postadressen.", "passwordResetCode": "Tilbakestillingskode", "passwordResetCodeDescription": "Sjekk e-posten din for tilbakestillingskoden.", + "generatePasswordResetCode": "Lag tilbakestillingskode for passord", + "passwordResetCodeGenerated": "Passord tilbakestillingskoden er generert", + "passwordResetCodeGeneratedDescription": "Del denne koden med brukeren. De kan bruke den til å tilbakestille passordet.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nytt passord", "passwordNewConfirm": "Bekreft nytt passord", "changePassword": "Endre passord", @@ -926,6 +945,9 @@ "pincodeAuth": "Autentiseringskode", "pincodeSubmit2": "Send inn kode", "passwordResetSubmit": "Be om tilbakestilling", + "passwordResetAlreadyHaveCode": "Skriv inn tilbakestillingskode for passord", + "passwordResetSmtpRequired": "Kontakt din administrator", + "passwordResetSmtpRequiredDescription": "En passord tilbakestillingskode kreves for å tilbakestille passordet. Kontakt systemansvarlig for assistanse.", "passwordBack": "Tilbake til passord", "loginBack": "Gå tilbake til innlogging", "signup": "Registrer deg", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "List opp Stedsressurser", "actionUpdateSiteResource": "Oppdater Stedsressurs", "actionListInvitations": "Liste invitasjoner", + "actionExportLogs": "Eksportlogger", + "actionViewLogs": "Vis logger", "noneSelected": "Ingen valgt", "orgNotFound2": "Ingen organisasjoner funnet.", "searchProgress": "Søker...", "create": "Opprett", "orgs": "Organisasjoner", "loginError": "En feil oppstod under innlogging", + "loginRequiredForDevice": "Innlogging kreves for å godkjenne enheten.", "passwordForgot": "Glemt passordet ditt?", "otpAuth": "Tofaktorautentisering", "otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Hjem", "sidebarSites": "Områder", "sidebarResources": "Ressurser", + "sidebarProxyResources": "Offentlig", + "sidebarClientResources": "Privat", "sidebarAccessControl": "Tilgangskontroll", + "sidebarLogsAndAnalytics": "Logger og analyser", "sidebarUsers": "Brukere", + "sidebarAdmin": "Administrator", "sidebarInvitations": "Invitasjoner", "sidebarRoles": "Roller", - "sidebarShareableLinks": "Delbare lenker", + "sidebarShareableLinks": "Lenker", "sidebarApiKeys": "API-nøkler", "sidebarSettings": "Innstillinger", "sidebarAllUsers": "Alle brukere", "sidebarIdentityProviders": "Identitetsleverandører", "sidebarLicense": "Lisens", "sidebarClients": "Klienter", + "sidebarUserDevices": "Brukere", + "sidebarMachineClients": "Maskiner", "sidebarDomains": "Domener", + "sidebarGeneral": "Generelt", + "sidebarLogAndAnalytics": "Logg og analyser", "sidebarBluePrints": "Tegninger", + "sidebarOrganization": "Organisasjon", + "sidebarLogsAnalytics": "Analyser", "blueprints": "Tegninger", "blueprintsDescription": "Bruk deklarative konfigurasjoner og vis tidligere kjøringer", "blueprintAdd": "Legg til blåkopi", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Se resultatet av den påførte blåkopien og alle feil som oppstod", "blueprintInfo": "Blåkopi informasjon", "message": "Melding", - "blueprintContentsDescription": "Definer innhold av YAML som beskriver din infrastruktur", + "blueprintContentsDescription": "Definere innholdet til YAML som beskriver infrastrukturen", "blueprintErrorCreateDescription": "Det oppstod en feil da plantegningen ble lagt til", "blueprintErrorCreate": "Feil ved opprettelse av plantegning", "searchBlueprintProgress": "Søk etter plantegninger...", @@ -1230,15 +1265,15 @@ "loading": "Laster inn", "restart": "Start på nytt", "domains": "Domener", - "domainsDescription": "Administrer domener for organisasjonen din", + "domainsDescription": "Opprett og behandle domener som er tilgjengelige i organisasjonen", "domainsSearch": "Søk i domener...", "domainAdd": "Legg til domene", - "domainAddDescription": "Registrer et nytt domene hos organisasjonen din", + "domainAddDescription": "Registrer et nytt domene med organisasjonen", "domainCreate": "Opprett domene", "domainCreatedDescription": "Domene ble opprettet", "domainDeletedDescription": "Domene ble slettet", - "domainQuestionRemove": "Er du sikker på at du vil fjerne domenet fra kontoen din?", - "domainMessageRemove": "Når domenet er fjernet, vil det ikke lenger være knyttet til kontoen din.", + "domainQuestionRemove": "Er du sikker på at du vil fjerne domenet?", + "domainMessageRemove": "Når domenet er fjernet, vil det ikke lenger være forbundet med organisasjonen.", "domainConfirmDelete": "Bekreft sletting av domene", "domainDelete": "Slett domene", "domain": "Domene", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Oppdateringer om produktet", "productUpdateEmpty": "Ingen oppdateringer", "dismissAll": "Avvis alle", - "pangolinUpdateAvailable": "Ny versjon tilgjengelig", + "pangolinUpdateAvailable": "Oppdatering tilgjengelig", "pangolinUpdateAvailableInfo": "Versjon {version} er klar til å installere", - "pangolinUpdateAvailableReleaseNotes": "Se utgivelsnotater", + "pangolinUpdateAvailableReleaseNotes": "Se utgivelsesnotater", "newtUpdateAvailable": "Oppdatering tilgjengelig", "newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.", "domainPickerEnterDomain": "Domene", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Å", "domainPickerSortDesc": "Å-A", "domainPickerCheckingAvailability": "Sjekker tilgjengelighet...", - "domainPickerNoMatchingDomains": "Ingen samsvarende domener funnet. Prøv et annet domene eller sjekk organisasjonens domeneinnstillinger.", + "domainPickerNoMatchingDomains": "Ingen samsvarende domener funnet. Prøv et annet domene eller kontroller organisasjonens domeneinnstillinger.", "domainPickerOrganizationDomains": "Organisasjonsdomener", "domainPickerProvidedDomains": "Leverte domener", "domainPickerSubdomain": "Underdomene: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Endre abonnement", "billingStartSubscription": "Start abonnement", "billingRecurringCharge": "Innkommende Avgift", - "billingManageSubscriptionSettings": "Administrer abonnementsinnstillinger og preferanser", + "billingManageSubscriptionSettings": "Administrer abonnementsinnstillinger og -innstillinger", "billingNoActiveSubscription": "Du har ikke et aktivt abonnement. Start abonnementet ditt for å øke bruksgrensene.", "billingFailedToLoadSubscription": "Klarte ikke å laste abonnement", "billingFailedToLoadUsage": "Klarte ikke å laste bruksdata", @@ -1345,9 +1380,9 @@ "billingPortalError": "Portalfeil", "billingDataUsageInfo": "Du er ladet for all data som overføres gjennom dine sikre tunneler når du er koblet til skyen. Dette inkluderer både innkommende og utgående trafikk på alle dine nettsteder. Når du når grensen din, vil sidene koble fra til du oppgraderer planen eller reduserer bruken. Data belastes ikke ved bruk av EK-grupper.", "billingOnlineTimeInfo": "Du er ladet på hvor lenge sidene dine forblir koblet til skyen. For eksempel tilsvarer 44,640 minutter ett nettsted som går 24/7 i en hel måned. Når du når grensen din, vil sidene koble fra til du oppgraderer planen eller reduserer bruken. Tid belastes ikke når du bruker noder.", - "billingUsersInfo": "Du belastes for hver bruker i organisasjonen din. Faktureringen beregnes daglig basert på antall aktive brukerkontoer i organisasjonen din.", - "billingDomainInfo": "Du belastes for hvert domene i organisasjonen din. Faktureringen beregnes daglig basert på antall aktive domenekontoer i organisasjonen din.", - "billingRemoteExitNodesInfo": "Du belastes for hver styrt node i organisasjonen din. Faktureringen beregnes daglig basert på antall aktive styrte noder i organisasjonen din.", + "billingUsersInfo": "Du lades for hver bruker i organisasjonen. Fakturering beregnes daglig basert på antall aktive brukerkontoer i dine org.", + "billingDomainInfo": "Du lades for hvert domene i organisasjonen. Fakturering beregnes daglig basert på antallet aktive domenekontoer i din org.", + "billingRemoteExitNodesInfo": "Du lades for hver håndterte node i organisasjonen. Fakturering beregnes daglig basert på antallet aktive håndterte noder i dine org.", "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", @@ -1430,29 +1465,32 @@ "and": "og", "privacyPolicy": "personvernerklæringen" }, + "signUpMarketing": { + "keepMeInTheLoop": "Hold meg i løken med nyheter, oppdateringer og nye funksjoner via e-post." + }, "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", + "createClientDescription": "Opprette en ny klient for å få tilgang til private ressurser", "seeAllClients": "Se alle klienter", "clientInformation": "Klientinformasjon", "clientNamePlaceholder": "Klientnavn", "address": "Adresse", "subnetPlaceholder": "Subnett", - "addressDescription": "Adressen denne klienten vil bruke for tilkobling", + "addressDescription": "Den interne adressen til klienten. Må falle innenfor organisasjonens undernett.", "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", + "clientOlmCredentials": "Legitimasjon", + "clientOlmCredentialsDescription": "Dette er hvordan klienten vil godkjenne med serveren", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Sikkerhetsnøkkel", + "clientCredentialsSave": "Lagre brukeropplysninger", "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", @@ -1463,9 +1501,7 @@ "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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Overvåk helsen til dette målet. Du kan overvåke et annet endepunkt enn målet hvis nødvendig.", "healthScheme": "Metode", "healthSelectScheme": "Velg metode", + "healthCheckPortInvalid": "Helsekontrollporten må være mellom 1 og 65535", "healthCheckPath": "Sti", "healthHostname": "IP / Vert", "healthPort": "Port", "healthCheckPathDescription": "Stien for å sjekke helsestatus.", - "healthyIntervalSeconds": "Sunt intervall", - "unhealthyIntervalSeconds": "Usunt intervall", + "healthyIntervalSeconds": "Sunn intervall (sek)", + "unhealthyIntervalSeconds": "Usunt intervall (sek)", "IntervalSeconds": "Sunt intervall", - "timeoutSeconds": "Tidsavbrudd", + "timeoutSeconds": "Tidsavbrudd (sek)", "timeIsInSeconds": "Tid er i sekunder", "retryAttempts": "Forsøk på nytt", "expectedResponseCodes": "Forventede svarkoder", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Rediger domene", "siteName": "Områdenavn", "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy-ressurser", - "resourcesTableClientResources": "Klientressurser", + "resourcesTableProxyResources": "Offentlig", + "resourcesTableClientResources": "Privat", "resourcesTableNoProxyResourcesFound": "Ingen proxy-ressurser funnet.", "resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.", "resourcesTableDestination": "Destinasjon", - "resourcesTableTheseResourcesForUseWith": "Disse ressursene er til bruk med", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Klienter", "resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.", "resourcesTableNoTargets": "Ingen mål", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Frakoblet", "resourcesTableUnknown": "Ukjent", "resourcesTableNotMonitored": "Ikke overvåket", - "editInternalResourceDialogEditClientResource": "Rediger klientressurs", - "editInternalResourceDialogUpdateResourceProperties": "Oppdater ressursens egenskaper og målkonfigurasjon for {resourceName}.", + "editInternalResourceDialogEditClientResource": "Rediger Private Ressurser", + "editInternalResourceDialogUpdateResourceProperties": "Oppdater ressurskonfigurasjonen og få tilgangskontroller for {resourceName}", "editInternalResourceDialogResourceProperties": "Ressursegenskaper", "editInternalResourceDialogName": "Navn", "editInternalResourceDialogProtocol": "Protokoll", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat", "editInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1", "editInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536", + "editInternalResourceDialogPortModeRequired": "Protokoll, proxy-port og målport er påkrevd for portmodus", + "editInternalResourceDialogMode": "Modus", + "editInternalResourceDialogModePort": "Port", + "editInternalResourceDialogModeHost": "Vert", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Destinasjon", + "editInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.", + "editInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.", + "editInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.", "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.", + "createInternalResourceDialogCreateClientResource": "Opprett privat ressurs", + "createInternalResourceDialogCreateClientResourceDescription": "Opprett en ny ressurs som bare vil være tilgjengelig for kunder som er koblet til organisasjonen", "createInternalResourceDialogResourceProperties": "Ressursegenskaper", "createInternalResourceDialogName": "Navn", "createInternalResourceDialogSite": "Område", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat", "createInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1", "createInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536", + "createInternalResourceDialogPortModeRequired": "Protokoll, proxy-port og målport er påkrevd for portmodus", + "createInternalResourceDialogMode": "Modus", + "createInternalResourceDialogModePort": "Port", + "createInternalResourceDialogModeHost": "Vert", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Destinasjon", + "createInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.", + "createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.", "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.", + "siteAcceptClientConnectionsDescription": "Tillat brukere og klienter å få tilgang til ressurser på denne siden. Dette kan endres senere.", + "siteAddress": "Nettstedsadresse (Avansert)", + "siteAddressDescription": "Den interne adressen til nettstedet. Må falle innenfor organisasjonens undernett.", + "siteNameDescription": "Visningsnavnet på nettstedet som kan endres senere.", "autoLoginExternalIdp": "Automatisk innlogging med ekstern IDP", "autoLoginExternalIdpDescription": "Omdiriger brukeren umiddelbart til den eksterne IDP-en for autentisering.", "selectIdp": "Velg IDP", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.", "autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.", "remoteExitNodeManageRemoteExitNodes": "Eksterne Noder", - "remoteExitNodeDescription": "Selvbetjent én eller flere eksterne noder for å utvide nettverkstilkoblingen din og redusere avhengighet på skyen", + "remoteExitNodeDescription": "Selvbetjent én eller flere eksterne noder for å utvide nettverkstilkobling og redusere avhengighet på skyen", "remoteExitNodes": "Noder", "searchRemoteExitNodes": "Søk noder...", "remoteExitNodeAdd": "Legg til Node", @@ -1623,7 +1682,7 @@ "sidebarRemoteExitNodes": "Eksterne Noder", "remoteExitNodeCreate": { "title": "Opprett node", - "description": "Opprett en ny node for å utvide nettverkstilkoblingen din", + "description": "Opprett en ny node for å utvide nettverkstilkoblingen", "viewAllButton": "Vis alle koder", "strategy": { "title": "Opprettelsesstrategi", @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Genererte Legitimasjoner", - "description": "Bruk disse genererte opplysningene for å konfigurere noden din", + "description": "Bruk disse genererte opplysningene for å konfigurere noden", "nodeIdTitle": "Node-ID", "secretTitle": "Sikkerhet", "saveCredentialsTitle": "Legg til Legitimasjoner til Config", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Identitet leverandør type", "roleMappingExpressionPlaceholder": "F.eks. inneholder(grupper, 'admin') && 'Admin' ⋅'Medlem'", "idpGoogleConfiguration": "Google Konfigurasjon", - "idpGoogleConfigurationDescription": "Konfigurer din Google OAuth2 legitimasjon", - "idpGoogleClientIdDescription": "Din Google OAuth2-klient-ID", - "idpGoogleClientSecretDescription": "Google OAuth2-klienten din hemmelig", + "idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimasjonen", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Google OAuth2-klienten hemmelighet", "idpAzureConfiguration": "Azure Entra ID konfigurasjon", - "idpAzureConfigurationDescription": "Konfigurere din Azure Entra ID OAuth2 legitimasjon", + "idpAzureConfigurationDescription": "Konfigurer Azure Entra ID OAuth2 legitimasjon", "idpTenantId": "Leietaker-ID", - "idpTenantIdPlaceholder": "din-tenant-id", - "idpAzureTenantIdDescription": "Din Azure leie-ID (funnet i Azure Active Directory-oversikten)", - "idpAzureClientIdDescription": "Din Azure App registrerings klient-ID", - "idpAzureClientSecretDescription": "Din Azure App registrerings klient hemmelig", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "Azure leant ID (funnet i Azure Active Directory-oversikten)", + "idpAzureClientIdDescription": "Azure App registrerings klient-ID", + "idpAzureClientSecretDescription": "Azure App Registrering Klient Hemmelig", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Google Konfigurasjon", "idpAzureConfigurationTitle": "Azure Entra ID konfigurasjon", "idpTenantIdLabel": "Leietaker-ID", - "idpAzureClientIdDescription2": "Din Azure App registrerings klient-ID", - "idpAzureClientSecretDescription2": "Din Azure App registrerings klient hemmelig", + "idpAzureClientIdDescription2": "Azure App registrerings klient-ID", + "idpAzureClientSecretDescription2": "Azure App Registrering Klient Hemmelig", "idpGoogleDescription": "Google OAuth2/OIDC leverandør", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Subnett", "subnetDescription": "Undernettverket for denne organisasjonens nettverkskonfigurasjon.", "authPage": "Autentiseringsside", - "authPageDescription": "Konfigurer autoriseringssiden for din organisasjon", + "authPageDescription": "Konfigurer autoriseringssiden for organisasjonen", "authPageDomain": "Autentiseringsside domene", "noDomainSet": "Ingen domene valgt", "changeDomain": "Endre domene", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Angi autoriseringsside domene", "failedToFetchCertificate": "Kunne ikke hente sertifikat", "failedToRestartCertificate": "Kan ikke starte sertifikat", - "addDomainToEnableCustomAuthPages": "Legg til et domene for å aktivere egendefinerte autentiseringssider for organisasjonen din", + "addDomainToEnableCustomAuthPages": "Legg til et domene for å aktivere egendefinerte autentiseringssider for organisasjonen", "selectDomainForOrgAuthPage": "Velg et domene for organisasjonens autentiseringsside", "domainPickerProvidedDomain": "Gitt domene", "domainPickerFreeProvidedDomain": "Gratis oppgitt domene", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" kunne ikke gjøres gyldig for {domain}.", "domainPickerSubdomainSanitized": "Underdomenet som ble sanivert", "domainPickerSubdomainCorrected": "\"{sub}\" var korrigert til \"{sanitized}\"", - "orgAuthSignInTitle": "Logg inn på din organisasjon", + "orgAuthSignInTitle": "Logg inn på organisasjonen", "orgAuthChooseIdpDescription": "Velg din identitet leverandør for å fortsette", "orgAuthNoIdpConfigured": "Denne organisasjonen har ikke noen identitetstjeneste konfigurert. Du kan i stedet logge inn med Pangolin identiteten din.", "orgAuthSignInWithPangolin": "Logg inn med Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Aktiver to-faktor autentisering", "completeSecuritySteps": "Fullfør sikkerhetstrinnene", "securitySettings": "Sikkerhet innstillinger", - "securitySettingsDescription": "Konfigurere sikkerhetspolicyer for din organisasjon", + "securitySettingsDescription": "Konfigurer sikkerhetspolicyer for organisasjonen", "requireTwoFactorForAllUsers": "Krev to-faktor autentisering for alle brukere", "requireTwoFactorDescription": "Når aktivert må alle interne brukere i denne organisasjonen ha to-faktorautentisering aktivert for å få tilgang til organisasjonen.", "requireTwoFactorDisabledDescription": "Denne funksjonen krever en gyldig lisens (Enterprise) eller aktivt abonnement (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Enterprise Edition", "unlicensed": "Ikke lisensiert", "beta": "beta", - "manageClients": "Administrer klienter", - "manageClientsDescription": "Klienter er enheter som kan koble seg til nettstedet ditt", + "manageUserDevices": "Bruker Enheter", + "manageUserDevicesDescription": "Se og administrer enheter som brukere bruker for privat tilkobling til ressurser", + "manageMachineClients": "Administrer maskinneklienter", + "manageMachineClientsDescription": "Opprett og behandle klienter som servere og systemer bruker for privat tilkobling til ressurser", + "clientsTableUserClients": "Bruker", + "clientsTableMachineClients": "Maskin", "licenseTableValidUntil": "Gyldig til", "saasLicenseKeysSettingsTitle": "Bedriftstillatelse Lisenser", "saasLicenseKeysSettingsDescription": "Generer og administrer Enterprise lisensnøkler for selvbetjente Pangolin forekomster", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "stripe", "sidebarEnableEnterpriseLicense": "Aktiver Enterprise lisens", "cannotbeUndone": "Dette kan ikke angres.", - "toConfirm": "å bekrefte", + "toConfirm": "å bekrefte.", "deleteClientQuestion": "Er du sikker på at du vil fjerne klienten fra nettstedet og organisasjonen?", "clientMessageRemove": "Når klienten er fjernet, kan den ikke lenger koble seg til nettstedet.", "sidebarLogs": "Logger", "request": "Forespørsel", + "requests": "Forespørsler", "logs": "Logger", "logsSettingsDescription": "Overvåk logger samlet fra denne orginiasjonen", "searchLogs": "Søk i logger...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Grunn", "requestLogs": "Forespørselslogger (Automatic Translation)", + "requestAnalytics": "Be om analyser", "host": "Vert", "location": "Sted", "actionLogs": "Handlingslogger", @@ -2029,6 +2094,7 @@ "logRetention": "Logg tilbaketrekning", "logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem", "requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen", + "requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen", "logRetentionRequestLabel": "Be om loggoverføring", "logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger", "logRetentionAccessLabel": "Få tilgang til loggoverføring", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 dager", "logRetention90Days": "90 dager", "logRetentionForever": "Alltid", + "logRetentionEndOfFollowingYear": "Slutt på neste år", "actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen", "accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen", "licenseRequiredToUse": "En Enterprise lisens er påkrevd for å bruke denne funksjonen.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Foretrekk Wildcard sertifikat", "unverified": "Uverifisert", "domainSetting": "Domene innstillinger", - "domainSettingDescription": "Konfigurer innstillinger for ditt domene", + "domainSettingDescription": "Konfigurer innstillinger for domenet", "preferWildcardCertDescription": "Forsøk på å generere et jokertegn (krever en riktig konfigurert sertifikatløsning).", "recordName": "Lagre navn", "auto": "Automatisk", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "En oppdatert versjon av Olm er tilgjengelig. Oppdater til den nyeste versjonen for å få den beste opplevelsen.", "client": "Klient", "proxyProtocol": "Protokoll innstillinger for Protokoll", - "proxyProtocolDescription": "Konfigurer Proxy-protokoll for å bevare klientens IP-adresser til TCP/UDP tjenester.", + "proxyProtocolDescription": "Konfigurer Proxy-protokoll for å bevare klientens IP-adresser til TCP-tjenester.", "enableProxyProtocol": "Aktiver Proxy-protokoll", - "proxyProtocolInfo": "Bevar klientens IP-adresser for TCP/UDP bakover", + "proxyProtocolInfo": "Bevar klientens IP-adresser for TCP backends", "proxyProtocolVersion": "Proxy protokoll versjon", "version1": " Versjon 1 (Anbefalt)", "version2": "Versjon 2", "versionDescription": "Versjon 1 er tekstbasert og støttet. Versjon 2 er binært og mer effektivt, men mindre kompatibel.", "warning": "Advarsel", - "proxyProtocolWarning": "Din backend applikasjon må være konfigurert for å godta Proxy Protokoller. Hvis din backend ikke støtter Proxy Protocol, vil aktivering av dette bryte alle tilkoblinger. Sørg for å konfigurere backend til å stole på Proxy Protokoll overskrifter fra Traefik.", + "proxyProtocolWarning": "backend-programmet må konfigureres til å akseptere forbindelser i Proxy Protokoll. Hvis backend ikke støtter Proxy Beskyttelse vil aktivering av dette ødelegge alle tilkoblinger så bare dette hvis du vet hva du gjør. Sørg for å konfigurere backend til å stole på Proxy Protokoll overskrifter fra Traefik.", "restarting": "Restarter...", "manual": "Manuell", "messageSupport": "Støtte for melding", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Melding sendt!", "supportWillContact": "Vi kommer raskt til å ta kontakt!", "selectLogRetention": "Velg oppbevaring av logg", + "terms": "Vilkår", + "privacy": "Personvern", + "security": "Sikkerhet", + "docs": "Dokumenter", + "deviceActivation": "Aktivering av enhet", + "deviceCodeInvalidFormat": "Kode må inneholde 9 tegn (f.eks A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Ugyldig eller utløpt kode", + "deviceCodeVerifyFailed": "Klarte ikke å bekrefte enhetskoden", + "signedInAs": "Logget inn som", + "deviceCodeEnterPrompt": "Skriv inn koden som vises på enheten", + "continue": "Fortsett", + "deviceUnknownLocation": "Ukjent plassering", + "deviceAuthorizationRequested": "Denne autorisasjonen ble forespurt fra {location} på {date}. Pass på at du stoler på denne enheten siden den vil få tilgang til kontoen.", + "deviceLabel": "Enhet: {deviceName}", + "deviceWantsAccess": "ønsker å få tilgang til kontoen din", + "deviceExistingAccess": "Eksisterende tilgang:", + "deviceFullAccess": "Full tilgang til din konto", + "deviceOrganizationsAccess": "Tilgang til alle organisasjoner din konto har tilgang til", + "deviceAuthorize": "Autoriser {applicationName}", + "deviceConnected": "Enhet tilkoblet!", + "deviceAuthorizedMessage": "Enhet er autorisert for tilgang til kontoen din.", + "pangolinCloud": "Pangolin Sky", + "viewDevices": "Vis enheter", + "viewDevicesDescription": "Administrer tilkoblede enheter", + "noDevices": "Ingen enheter funnet", + "dateCreated": "Opprettet dato", + "unnamedDevice": "Navnløs enhet", + "deviceQuestionRemove": "Er du sikker på at du vil slette denne enheten?", + "deviceMessageRemove": "Denne handlingen kan ikke angres.", + "deviceDeleteConfirm": "Slett enhet", + "deleteDevice": "Slett enhet", + "errorLoadingDevices": "Feil ved lasting av enheter", + "failedToLoadDevices": "Klarte ikke å laste enheter", + "deviceDeleted": "Enheten er slettet", + "deviceDeletedDescription": "Enheten har blitt slettet.", + "errorDeletingDevice": "Feil ved sletting av enhet", + "failedToDeleteDevice": "Kunne ikke slette enheten", "showColumns": "Vis kolonner", "hideColumns": "Skjul kolonner", "columnVisibility": "Kolonne Synlighet", @@ -2111,10 +2215,14 @@ "enableSelected": "Aktiver valgte", "disableSelected": "Deaktiver valgte", "checkSelectedStatus": "Kontroller status for valgte", + "clients": "Klienter", + "accessClientSelect": "Velg maskinklienter", + "resourceClientDescription": "Maskinklienter som har tilgang til denne ressursen", + "regenerate": "Regenerer", "credentials": "Legitimasjon", "savecredentials": "Lagre brukeropplysninger", - "regeneratecredentials": "Ny nøkkel", - "regenerateCredentials": "Regenerer og lagre opplysningene dine", + "regenerateCredentialsButton": "Regenerer brukeropplysninger", + "regenerateCredentials": "Regenerer brukeropplysninger", "generatedcredentials": "Genererte brukeropplysninger", "copyandsavethesecredentials": "Kopier og lagre disse opplysningene", "copyandsavethesecredentialsdescription": "Disse opplysningene vil ikke bli vist igjen etter at du forlater siden. Lagre dem trygt nå.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Påloggingsinformasjonen har blitt regenerert og lagret.", "credentialsSaveError": "Påloggingsinformasjon lagre feil", "credentialsSaveErrorDescription": "En feil oppstod under regenerering og lagring av legitimasjon.", - "regenerateCredentialsWarning": "Regenerering av legitimasjon vil ugyldiggjøre de forrige. Sørg for at alle konfigurasjoner som bruker disse legitimasjonene.", + "regenerateCredentialsWarning": "Regenerering av legitimasjon vil ugyldiggjøre de forrige og forårsake en frakobling. Sørg for å oppdatere alle konfigurasjoner som bruker disse legitimasjonene.", "confirm": "Bekreft", "regenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legetimasjonene?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Hemmelig nøkkel", - "featureDisabledTooltip": "Denne funksjonen er bare tilgjengelig i virksomhetsplanen og krever en lisens til å bruke den.", "niceId": "God ID", "niceIdUpdated": "Flott ID oppdatert", "niceIdUpdatedSuccessfully": "Id-en ble oppdatert", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Det oppstod en feil under oppdatering av Nice ID.", "niceIdCannotBeEmpty": "God ID kan ikke være tom", "enterIdentifier": "Angi identifikator", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "Ikke du? Bruk en annen konto.", + "deviceLoginDeviceRequestingAccessToAccount": "En enhet ber om tilgang til denne kontoen.", + "noData": "Ingen data", + "machineClients": "Maskinklienter", + "install": "Installer", + "run": "Kjør", + "clientNameDescription": "Visningsnavnet til klienten som kan endres senere.", + "clientAddress": "Klientadresse (avansert)", + "setupFailedToFetchSubnet": "Kunne ikke hente standard undernett", + "setupSubnetAdvanced": "Subnet (avansert)", + "setupSubnetDescription": "Subnet for denne organisasjonens interne nettverk.", + "siteRegenerateAndDisconnect": "Regenerer og koble fra", + "siteRegenerateAndDisconnectConfirmation": "Er du sikker på at du vil regenerere legitimasjon og koble fra dette nettstedet?", + "siteRegenerateAndDisconnectWarning": "Dette vil regenerere legitimasjon og umiddelbart koble fra siden. Siden må startes på nytt med de nye legitimasjonene.", + "siteRegenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legitimasjon for dette nettstedet?", + "siteRegenerateCredentialsWarning": "Dette vil regenerere legitimasjonen. Siden vil forbli tilkoblet inntil du manuelt starter den på nytt og bruker de nye legitimasjonen.", + "clientRegenerateAndDisconnect": "Regenerer og koble fra", + "clientRegenerateAndDisconnectConfirmation": "Er du sikker på at du vil regenerere legitimasjon og koble fra denne klienten?", + "clientRegenerateAndDisconnectWarning": "Dette vil regenerere legitimasjon og umiddelbart koble fra klienten. Kunden må startes på nytt med de nye legitimasjonene.", + "clientRegenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legitimasjon for denne klienten?", + "clientRegenerateCredentialsWarning": "Dette vil regenerere legitimasjon. Klienten vil forbli tilkoblet inntil du manuelt starter den på nytt og bruker de nye legitimasjonen.", + "remoteExitNodeRegenerateAndDisconnect": "Regenerer og koble fra", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Er du sikker på at du vil regenerere legitimasjon og koble fra denne eksterne avslutnings noden?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Dette vil regenerere innloggingsdetaljene og umiddelbart koble fra den eksterne avkjøringnoden. Ekstern avkjøringsnode må startes på nytt med de nye opplysningene", + "remoteExitNodeRegenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere innloggingsene for denne eksterne avslutningen?", + "remoteExitNodeRegenerateCredentialsWarning": "Dette vil regenerere legitimasjon. Ekstern avgang noden vil forbli tilkoblet inntil du manuelt gjenoppstarter den og bruker de nye legitimasjonene.", + "agent": "Agent" } diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 73ba2acc..c235676a 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -1,12 +1,12 @@ { - "setupCreate": "Maak uw organisatie, site en bronnen aan", + "setupCreate": "Maak de organisatie, site en bronnen aan", "setupNewOrg": "Nieuwe organisatie", "setupCreateOrg": "Nieuwe organisatie aanmaken", "setupCreateResources": "Bronnen aanmaken", "setupOrgName": "Naam van de organisatie", - "orgDisplayName": "Dit is de weergavenaam van uw organisatie.", + "orgDisplayName": "Dit is de weergavenaam van de organisatie.", "orgId": "Organisatie ID", - "setupIdentifierMessage": "Dit is de unieke identificatie voor uw organisatie. Deze is gescheiden van de weergavenaam.", + "setupIdentifierMessage": "Dit is de unieke identificatie voor de organisatie.", "setupErrorIdentifier": "Organisatie-ID is al in gebruik. Kies een andere.", "componentsErrorNoMemberCreate": "U bent momenteel geen lid van een organisatie. Maak een organisatie aan om aan de slag te gaan.", "componentsErrorNoMember": "U bent momenteel geen lid van een organisatie.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Eenmaal verwijderd zal de site niet langer toegankelijk zijn. Alle aan de site gekoppelde doelen zullen ook worden verwijderd.", "siteQuestionRemove": "Weet u zeker dat u de site wilt verwijderen uit de organisatie?", "siteManageSites": "Sites beheren", - "siteDescription": "Verbindt met uw netwerk via beveiligde tunnels", + "siteDescription": "Maak en beheer sites om verbinding met privénetwerken in te schakelen", "siteCreate": "Site maken", "siteCreateDescription2": "Volg de onderstaande stappen om een nieuwe site aan te maken en te verbinden", - "siteCreateDescription": "Maak een nieuwe site aan om verbinding te maken met uw bronnen", + "siteCreateDescription": "Maak een nieuwe site aan om bronnen te verbinden", "close": "Sluiten", "siteErrorCreate": "Fout bij maken site", "siteErrorCreateKeyPair": "Key pair of site standaard niet gevonden", @@ -74,7 +74,7 @@ "siteInstallNewt": "Installeer Newt", "siteInstallNewtDescription": "Laat Newt draaien op uw systeem", "WgConfiguration": "WireGuard Configuratie", - "WgConfigurationDescription": "Gebruik de volgende configuratie om verbinding te maken met je netwerk", + "WgConfigurationDescription": "Gebruik de volgende configuratie om verbinding te maken met het netwerk", "operatingSystem": "Operating systeem", "commands": "Opdrachten", "recommended": "Aanbevolen", @@ -87,32 +87,32 @@ "siteUpdated": "Site bijgewerkt", "siteUpdatedDescription": "De site is bijgewerkt.", "siteGeneralDescription": "Algemene instellingen voor deze site configureren", - "siteSettingDescription": "Configureer de instellingen op uw site", + "siteSettingDescription": "Configureer de instellingen van de site", "siteSetting": "{siteName} instellingen", - "siteNewtTunnel": "Newttunnel (Aanbevolen)", - "siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.", + "siteNewtTunnel": "Nieuwste site (Aanbevolen)", + "siteNewtTunnelDescription": "Makkelijkste manier om een ingangspunt in een netwerk te maken. Geen extra opzet.", "siteWg": "Basis WireGuard", "siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.", "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": "Enkel lokale bronnen. Geen tunneling. Alleen beschikbaar op externe knooppunten.", "siteSeeAll": "Alle sites bekijken", - "siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site", - "siteNewtCredentials": "Nieuwste aanmeldgegevens", - "siteNewtCredentialsDescription": "Dit is hoe Newt zich zal verifiëren met de server", - "siteCredentialsSave": "Uw referenties opslaan", + "siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met de site", + "siteNewtCredentials": "Aanmeldgegevens", + "siteNewtCredentialsDescription": "Dit is hoe de site zich zal verifiëren met de server", + "siteCredentialsSave": "Sla de aanmeldgegevens op", "siteCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.", "siteInfo": "Site informatie", "status": "Status", "shareTitle": "Beheer deellinks", - "shareDescription": "Maak deelbare links aan om tijdelijke of permanente toegang tot uw bronnen te verlenen", + "shareDescription": "Maak deelbare links aan om tijdelijke of permanente toegang tot proxybronnen te verlenen", "shareSearch": "Zoek share links...", "shareCreate": "Maak Share link", "shareErrorDelete": "Kan link niet verwijderen", "shareErrorDeleteMessage": "Fout opgetreden tijdens het verwijderen link", "shareDeleted": "Link verwijderd", "shareDeletedDescription": "De link is verwijderd", - "shareTokenDescription": "Uw toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de header van de aanvraag. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.", + "shareTokenDescription": "De toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de aanvraagheaders. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.", "accessToken": "Toegangs-token", "usageExamples": "Voorbeelden van gebruik", "tokenId": "Token ID", @@ -121,7 +121,7 @@ "importantNote": "Belangrijke opmerking", "shareImportantDescription": "Om veiligheidsredenen wordt het gebruik van headers aanbevolen over queryparameters indien mogelijk, omdat query parameters kunnen worden aangemeld in serverlogboeken of browsergeschiedenis.", "token": "Token", - "shareTokenSecurety": "Houd uw toegangstoken veilig. Deel deze niet in openbaar toegankelijke gebieden of client-side code.", + "shareTokenSecurety": "Houd het toegangstoken beveiligd. Deel het niet in openbaar toegankelijke gebieden of client-side code.", "shareErrorFetchResource": "Fout bij het ophalen van bronnen", "shareErrorFetchResourceDescription": "Er is een fout opgetreden bij het ophalen van de resources", "shareErrorCreate": "Aanmaken van link delen mislukt", @@ -144,8 +144,10 @@ "expires": "Verloopt", "never": "Nooit", "shareErrorSelectResource": "Selecteer een bron", - "resourceTitle": "Bronnen beheren", - "resourceDescription": "Veilige proxy's voor uw privéapplicaties maken", + "proxyResourceTitle": "Openbare bronnen beheren", + "proxyResourceDescription": "Creëer en beheer bronnen die openbaar toegankelijk zijn via een webbrowser", + "clientResourceTitle": "Privébronnen beheren", + "clientResourceDescription": "Creëer en beheer bronnen die alleen toegankelijk zijn via een verbonden client", "resourcesSearch": "Zoek bronnen...", "resourceAdd": "Bron toevoegen", "resourceErrorDelte": "Fout bij verwijderen document", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Eenmaal verwijderd, zal het bestand niet langer toegankelijk zijn. Alle doelen die gekoppeld zijn aan het hulpbron, zullen ook verwijderd worden.", "resourceQuestionRemove": "Weet u zeker dat u het document van de organisatie wilt verwijderen?", "resourceHTTP": "HTTPS bron", - "resourceHTTPDescription": "Proxy verzoeken aan uw app via HTTPS via een subdomein of basisdomein.", + "resourceHTTPDescription": "Proxy verzoeken aan de app via HTTPS via een subdomein of basisdomein.", "resourceRaw": "TCP/UDP bron", - "resourceRawDescription": "Proxy verzoeken naar je app via TCP/UDP met behulp van een poortnummer.", + "resourceRawDescription": "Proxy verzoeken naar de app via TCP/UDP met behulp van een poortnummer. Dit werkt alleen als sites zijn verbonden met nodes.", "resourceCreate": "Bron maken", "resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken", "resourceSeeAll": "Alle bronnen bekijken", @@ -171,22 +173,22 @@ "noCountryFound": "Geen land gevonden.", "siteSelectionDescription": "Deze site zal connectiviteit met het doelwit bieden.", "resourceType": "Type bron", - "resourceTypeDescription": "Bepaal hoe u toegang wilt krijgen tot uw bron", + "resourceTypeDescription": "Bepaal hoe u toegang wilt tot de bron", "resourceHTTPSSettings": "HTTPS instellingen", "resourceHTTPSSettingsDescription": "Stel in hoe de bron wordt benaderd via HTTPS", "domainType": "Domein type", "subdomain": "Subdomein", "baseDomain": "Basis domein", - "subdomnainDescription": "Het subdomein waar de bron toegankelijk is.", + "subdomnainDescription": "Het subdomein waar de bron toegankelijk zal zijn.", "resourceRawSettings": "TCP/UDP instellingen", - "resourceRawSettingsDescription": "Stel in hoe uw bron wordt benaderd via TCP/UDP. Je gooit de bron toe aan een poort op de host-Pangolin server, zodat je de bron kan bereiken vanaf server-public-ip:mapped-port.", + "resourceRawSettingsDescription": "Stel in hoe de bron wordt benaderd via TCP/UDP", "protocol": "Protocol", "protocolSelect": "Selecteer een protocol", "resourcePortNumber": "Nummer van poort", "resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.", "cancel": "Annuleren", "resourceConfig": "Configuratie tekstbouwstenen", - "resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om je TCP/UDP-bron in te stellen", + "resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om de TCP/UDP-bron in te stellen", "resourceAddEntrypoints": "Traefik: Entrypoints toevoegen", "resourceExposePorts": "Gerbild: Gevangen blootstellen in Docker Compose", "resourceLearnRaw": "Leer hoe je TCP/UDP bronnen kunt configureren", @@ -202,14 +204,14 @@ "proxy": "Proxy", "internal": "Intern", "rules": "Regels", - "resourceSettingDescription": "Configureer de instellingen op uw bron", + "resourceSettingDescription": "Configureer de instellingen in de bron", "resourceSetting": "{resourceName} instellingen", - "alwaysAllow": "Altijd toestaan", - "alwaysDeny": "Altijd weigeren", + "alwaysAllow": "Authenticatie omzeilen", + "alwaysDeny": "Blokkeer toegang", "passToAuth": "Passeren naar Auth", - "orgSettingsDescription": "Configureer de algemene instellingen van je organisatie", + "orgSettingsDescription": "Configureer de instellingen van de organisatie", "orgGeneralSettings": "Organisatie Instellingen", - "orgGeneralSettingsDescription": "Beheer de details en configuratie van uw organisatie", + "orgGeneralSettingsDescription": "De details en configuratie van de organisatie beheren", "saveGeneralSettings": "Algemene instellingen opslaan", "saveSettings": "Instellingen opslaan", "orgDangerZone": "Gevaarlijke zone", @@ -232,7 +234,7 @@ "orgMissing": "Organisatie-ID ontbreekt", "orgMissingMessage": "Niet in staat om de uitnodiging te regenereren zonder organisatie-ID.", "accessUsersManage": "Gebruikers beheren", - "accessUsersDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot uw organisatie te beheren", + "accessUsersDescription": "Nodig uit en beheer gebruikers met toegang tot deze organisatie", "accessUsersSearch": "Gebruikers zoeken...", "accessUserCreate": "Gebruiker aanmaken", "accessUserRemove": "Gebruiker verwijderen", @@ -241,13 +243,13 @@ "role": "Functie", "nameRequired": "Naam is verplicht", "accessRolesManage": "Rollen beheren", - "accessRolesDescription": "Configureer rollen om toegang tot uw organisatie te beheren", + "accessRolesDescription": "Maak en beheer rollen voor gebruikers in de organisatie", "accessRolesSearch": "Rollen zoeken...", "accessRolesAdd": "Rol toevoegen", "accessRoleDelete": "Verwijder rol", "description": "Beschrijving", "inviteTitle": "Open uitnodigingen", - "inviteDescription": "Beheer je uitnodigingen aan andere gebruikers", + "inviteDescription": "Beheer uitnodigingen voor andere gebruikers om deel te nemen aan de organisatie", "inviteSearch": "Uitnodigingen zoeken...", "minutes": "minuten", "hours": "Uren", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Fout bij maken API-sleutel", "apiKeysErrorSetPermission": "Fout instellen permissies", "apiKeysCreate": "API-sleutel genereren", - "apiKeysCreateDescription": "Genereer een nieuwe API-sleutel voor uw organisatie", + "apiKeysCreateDescription": "Een nieuwe API-sleutel voor de organisatie genereren", "apiKeysGeneralSettings": "Machtigingen", "apiKeysGeneralSettingsDescription": "Bepaal wat deze API-sleutel kan doen", - "apiKeysList": "Uw API-sleutel", - "apiKeysSave": "Uw API-sleutel opslaan", + "apiKeysList": "Nieuwe API-sleutel", + "apiKeysSave": "De API-sleutel opslaan", "apiKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een veilige plek.", - "apiKeysInfo": "Uw API-sleutel is:", + "apiKeysInfo": "De API-sleutel is:", "apiKeysConfirmCopy": "Ik heb de API-sleutel gekopieerd", "generate": "Genereren", "done": "Voltooid", @@ -424,7 +426,7 @@ "userCreated": "Gebruiker aangemaakt", "userCreatedDescription": "De gebruiker is succesvol aangemaakt.", "userTypeInternal": "Interne gebruiker", - "userTypeInternalDescription": "Nodig een gebruiker uit om direct lid te worden van je organisatie.", + "userTypeInternalDescription": "Nodig een gebruiker uit om direct lid te worden van de organisatie.", "userTypeExternal": "Externe gebruiker", "userTypeExternalDescription": "Maak een gebruiker aan met een externe identiteitsprovider.", "accessUserCreateDescription": "Volg de onderstaande stappen om een nieuwe gebruiker te maken", @@ -436,6 +438,16 @@ "inviteEmailSent": "Stuur uitnodigingsmail naar de gebruiker", "inviteValid": "Geldig voor", "selectDuration": "Selecteer duur", + "selectResource": "Selecteer Document", + "filterByResource": "Filter op pagina", + "resetFilters": "Filters resetten", + "totalBlocked": "Verzoeken geblokkeerd door Pangolin", + "totalRequests": "Totaal verzoeken", + "requestsByCountry": "Verzoeken per land", + "requestsByDay": "Verzoeken per dag", + "blocked": "Geblokkeerd", + "allowed": "Toegestaan", + "topCountries": "Top Landen", "accessRoleSelect": "Selecteer rol", "inviteEmailSentDescription": "Een e-mail is verstuurd naar de gebruiker met de link hieronder. Ze moeten toegang krijgen tot de link om de uitnodiging te accepteren.", "inviteSentDescription": "De gebruiker is uitgenodigd. Ze moeten toegang krijgen tot de link hieronder om de uitnodiging te accepteren.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Bewaar Toegangsbesturing", "roles": "Rollen", "accessUsersRoles": "Beheer Gebruikers & Rollen", - "accessUsersRolesDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot uw organisatie te beheren", + "accessUsersRolesDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot de organisatie te beheren", "key": "Sleutel", "createdAt": "Aangemaakt op", "proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.", "proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.", "proxyEnableSSL": "SSL inschakelen", - "proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar uw doelen.", + "proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar de doelen.", "target": "Target", "configureTarget": "Doelstellingen configureren", "targetErrorFetch": "Ophalen van doelen mislukt", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Kan doelen niet bijwerken", "targetsErrorUpdateDescription": "Fout opgetreden tijdens het bijwerken van de doelen", "targetTlsUpdate": "TLS instellingen bijgewerkt", - "targetTlsUpdateDescription": "Uw TLS instellingen zijn succesvol bijgewerkt", + "targetTlsUpdateDescription": "TLS instellingen zijn succesvol bijgewerkt", "targetErrorTlsUpdate": "Bijwerken van TLS instellingen mislukt", "targetErrorTlsUpdateDescription": "Fout opgetreden tijdens het bijwerken van de TLS-instellingen", "proxyUpdated": "Proxyinstellingen bijgewerkt", - "proxyUpdatedDescription": "Uw proxyinstellingen zijn succesvol bijgewerkt", + "proxyUpdatedDescription": "Proxyinstellingen zijn succesvol bijgewerkt", "proxyErrorUpdate": "Bijwerken van proxy-instellingen mislukt", "proxyErrorUpdateDescription": "Fout opgetreden tijdens het bijwerken van de proxy-instellingen", - "targetAddr": "IP / Hostnaam", + "targetAddr": "Hostnaam", "targetPort": "Poort", "targetProtocol": "Protocol", "targetTlsSettings": "HTTPS & TLS instellingen", - "targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren", + "targetTlsSettingsDescription": "Configureer SSL/TLS instellingen voor de bron", "targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen", "targetTlsSni": "TLS servernaam", "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 backend-services te leiden", + "targetsDescription": "Stel doelen in om verkeer naar backend diensten te sturen", "targetStickySessions": "Sticky sessies inschakelen", "targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.", "methodSelect": "Selecteer methode", "targetSubmit": "Doelwit toevoegen", - "targetNoOne": "Deze bron heeft geen doelen. Voeg een doel toe om te configureren waar verzoeken naar uw backend.", + "targetNoOne": "Deze bron heeft geen doelwitten. Voeg een doel toe om te configureren waar verzoeken naar de backend verzonden kunnen worden.", "targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.", "targetsSubmit": "Doelstellingen opslaan", "addTarget": "Doelwit toevoegen", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Doel is succesvol aangemaakt", "targetErrorCreate": "Kan doel niet aanmaken", "targetErrorCreateDescription": "Fout opgetreden tijdens het aanmaken van het doel", + "tlsServerName": "TLS servernaam", + "tlsServerNameDescription": "De TLS servernaam om te gebruiken voor SNI", "save": "Opslaan", "proxyAdditional": "Extra Proxy-instellingen", - "proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld", + "proxyAdditionalDescription": "Configureer hoe de bron omgaat met proxy-instellingen", "proxyCustomHeader": "Aangepaste Host-header", "proxyCustomHeaderDescription": "De hostkop om in te stellen bij proxying verzoeken. Laat leeg om de standaard te gebruiken.", "proxyAdditionalSubmit": "Proxyinstellingen opslaan", @@ -558,7 +572,7 @@ "rulesMatchType": "Wedstrijd Type", "value": "Waarde", "rulesAbout": "Over regels", - "rulesAboutDescription": "Regels stellen u in staat om de toegang tot uw bron te controleren op basis van een aantal criteria. U kunt regels maken om toegang te toestaan of weigeren op basis van IP-adres of URL pad.", + "rulesAboutDescription": "Regels stellen u in staat om de toegang tot het bestand te controleren op basis van een aantal criteria. U kunt regels maken om toegang te toestaan of weigeren op basis van IP-adres of URL pad.", "rulesActions": "acties", "rulesActionAlwaysAllow": "Altijd toegestaan: Omzeil alle authenticatiemethoden", "rulesActionAlwaysDeny": "Altijd weigeren: Blokkeer alle aanvragen, er kan geen verificatie worden geprobeerd", @@ -570,7 +584,7 @@ "rulesEnable": "Regels inschakelen", "rulesEnableDescription": "In- of uitschakelen van regelevaluatie voor deze bron", "rulesResource": "Configuratie Resource Regels", - "rulesResourceDescription": "Regels instellen om toegang tot uw bron te beheren", + "rulesResourceDescription": "Regels instellen om toegang tot de bron te beheren", "ruleSubmit": "Regel toevoegen", "rulesNoOne": "Geen regels. Voeg een regel toe via het formulier.", "rulesOrder": "Regels worden in oplopende volgorde volgens prioriteit beoordeeld.", @@ -586,7 +600,7 @@ "none": "geen", "unknown": "onbekend", "resources": "Bronnen", - "resourcesDescription": "Bronnen zijn proxies voor applicaties die op uw privénetwerk worden uitgevoerd. Maak een bron aan voor elke HTTP/HTTPS of onbewerkte TCP/UDP-service op uw privénetwerk. Elke bron moet verbonden zijn met een site om private, beveiligde verbinding mogelijk te maken via een versleutelde WireGuard tunnel.", + "resourcesDescription": "Bronnen zijn proxies voor applicaties die op het privénetwerk worden uitgevoerd. Maak een bron aan voor een HTTP/HTTPS of ruwe TCP/UDP-service op uw privénetwerk. Elke bron moet verbonden zijn met een site om private, beveiligde verbinding mogelijk te maken via een versleutelde WireGuard tunnel.", "resourcesWireGuardConnect": "Beveiligde verbinding met WireGuard versleuteling", "resourcesMultipleAuthenticationMethods": "Meerdere verificatiemethoden configureren", "resourcesUsersRolesAccess": "Gebruiker en rol-gebaseerde toegangsbeheer", @@ -597,7 +611,7 @@ "resourceSelect": "Selecteer resource", "shareLinks": "Links delen", "share": "Deelbare links", - "shareDescription2": "Maak deelbare links naar uw bronnen. Links bieden tijdelijke of onbeperkte toegang tot uw bron. U kunt de vervalduur van de link configureren wanneer u er een aanmaakt.", + "shareDescription2": "Maak deelbare links naar bronnen. Links bieden tijdelijke of onbeperkte toegang tot je bestand. U kunt de vervalduur van de link configureren wanneer u er een aanmaakt.", "shareEasyCreate": "Makkelijk te maken en te delen", "shareConfigurableExpirationDuration": "Configureerbare vervalduur", "shareSecureAndRevocable": "Veilig en herroepbaar", @@ -607,19 +621,19 @@ "unknownCommand": "Onbekende opdracht", "newtErrorFetchReleases": "Kan release-informatie niet ophalen: {err}", "newtErrorFetchLatest": "Fout bij ophalen van laatste release: {err}", - "newtEndpoint": "Newt Eindoordeel", - "newtId": "Newt-ID", - "newtSecretKey": "Nieuwe geheime sleutel", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Geheim", "architecture": "Architectuur", "sites": "Sites", - "siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je moet je interne bronnen aanspreken met behulp van de peer IP.", + "siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je zult interne bronnen moeten aanspreken met behulp van de peer IP.", "siteWgCompatibleAllClients": "Compatibel met alle WireGuard clients", "siteWgManualConfigurationRequired": "Handmatige configuratie vereist", "userErrorNotAdminOrOwner": "Gebruiker is geen beheerder of eigenaar", "pangolinSettings": "Instellingen - Pangolin", "accessRoleYour": "Jouw rol:", - "accessRoleSelect2": "Selecteer lidmaatschap", - "accessUserSelect": "Selecteer een gebruiker", + "accessRoleSelect2": "Selecteer rollen", + "accessUserSelect": "Gebruikers selecteren", "otpEmailEnter": "Voer e-mailadres in", "otpEmailEnterDescription": "Druk op enter om een e-mail toe te voegen na het typen in het invoerveld.", "otpEmailErrorInvalid": "Ongeldig e-mailadres. Wildcard (*) moet het hele lokale deel zijn.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Pincode instellen", "resourcePincodeSetupTitleDescription": "Stel een pincode in om deze hulpbron te beschermen", "resourceRoleDescription": "Beheerders hebben altijd toegang tot deze bron.", - "resourceUsersRoles": "Gebruikers & Rollen", + "resourceUsersRoles": "Toegang Bediening", "resourceUsersRolesDescription": "Configureer welke gebruikers en rollen deze pagina kunnen bezoeken", "resourceUsersRolesSubmit": "Gebruikers opslaan & rollen", "resourceWhitelistSave": "Succesvol opgeslagen", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Bronnen overdragen", "siteDestination": "Bestemming site", "searchSites": "Sites zoeken", + "countries": "Landen", "accessRoleCreate": "Rol aanmaken", "accessRoleCreateDescription": "Maak een nieuwe rol aan om gebruikers te groeperen en hun rechten te beheren.", "accessRoleCreateSubmit": "Rol aanmaken", @@ -766,15 +781,15 @@ "idpOidcConfigure": "OAuth2/OIDC configuratie", "idpOidcConfigureDescription": "Configureer de eindpunten van de OAuth2/OIDC provider en referenties", "idpClientId": "Client ID", - "idpClientIdDescription": "De OAuth2 client ID van uw identiteitsprovider", + "idpClientIdDescription": "De OAuth2-client-ID van de identiteitsaanbieder", "idpClientSecret": "Client Secret", - "idpClientSecretDescription": "Het OAuth2 Client Secret van je identiteitsprovider", + "idpClientSecretDescription": "Het OAuth2-clientgeheim van de identiteitsprovider", "idpAuthUrl": "URL autorisatie", "idpAuthUrlDescription": "De URL voor autorisatie OAuth2", "idpTokenUrl": "URL token", "idpTokenUrlDescription": "De URL van het OAuth2 token eindpunt", "idpOidcConfigureAlert": "Belangrijke informatie", - "idpOidcConfigureAlertDescription": "Na het aanmaken van de identity provider moet u de callback URL configureren in de instellingen van uw identity provider. De callback URL zal worden opgegeven na het succesvol aanmaken.", + "idpOidcConfigureAlertDescription": "Na het aanmaken van de identity provider moet u de callback URL configureren in de instellingen van de identity provider. De callback URL zal worden opgegeven na het succesvol aanmaken.", "idpToken": "Token configuratie", "idpTokenDescription": "Stel in hoe gebruikersgegevens uit het ID token uit te pakken", "idpJmespathAbout": "Over JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Identity Provider aanmaken", "orgPolicies": "Organisatie beleid", "idpSettings": "{idpName} instellingen", - "idpCreateSettingsDescription": "Configureer de instellingen voor uw identiteitsprovider", + "idpCreateSettingsDescription": "Configureer de instellingen voor de identiteitsprovider", "roleMapping": "Rol Toewijzing", "orgMapping": "Organisatie toewijzing", "orgPoliciesSearch": "Zoek het organisatiebeleid...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Identity provider succesvol bijgewerkt", "redirectUrl": "Omleidings URL", "redirectUrlAbout": "Over omleidings-URL", - "redirectUrlAboutDescription": "Dit is de URL waarnaar gebruikers worden doorverwezen na verificatie. U moet deze URL configureren in uw identiteitsprovider-instellingen.", + "redirectUrlAboutDescription": "Dit is de URL waarnaar gebruikers worden doorverwezen na verificatie. U moet deze URL configureren in de instellingen van de identiteitsprovider.", "pangolinAuth": "Authenticatie - Pangolin", "verificationCodeLengthRequirements": "Je verificatiecode moet 8 tekens bevatten.", "errorOccurred": "Er is een fout opgetreden", @@ -909,6 +924,10 @@ "passwordResetSent": "We sturen een wachtwoord reset code naar dit e-mailadres.", "passwordResetCode": "Resetcode", "passwordResetCodeDescription": "Controleer je e-mail voor de reset code.", + "generatePasswordResetCode": "Herstelcode voor wachtwoord genereren", + "passwordResetCodeGenerated": "Wachtwoord reset code gegenereerd", + "passwordResetCodeGeneratedDescription": "Deel deze code met de gebruiker. Ze kunnen deze gebruiken om hun wachtwoord te resetten.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nieuw wachtwoord", "passwordNewConfirm": "Bevestig nieuw wachtwoord", "changePassword": "Wachtwoord wijzigen", @@ -926,6 +945,9 @@ "pincodeAuth": "Authenticatiecode", "pincodeSubmit2": "Code indienen", "passwordResetSubmit": "Opnieuw instellen aanvragen", + "passwordResetAlreadyHaveCode": "Herstelcode wachtwoord invoeren", + "passwordResetSmtpRequired": "Neem contact op met uw beheerder", + "passwordResetSmtpRequiredDescription": "Er is een wachtwoord reset code nodig om uw wachtwoord opnieuw in te stellen. Neem contact op met uw beheerder voor hulp.", "passwordBack": "Terug naar wachtwoord", "loginBack": "Ga terug naar login", "signup": "Registreer nu", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Bronnen van site weergeven", "actionUpdateSiteResource": "Document bijwerken van site", "actionListInvitations": "Toon uitnodigingen", + "actionExportLogs": "Logboeken exporteren", + "actionViewLogs": "Logboeken bekijken", "noneSelected": "Niet geselecteerd", "orgNotFound2": "Geen organisaties gevonden.", "searchProgress": "Zoeken...", "create": "Aanmaken", "orgs": "Organisaties", "loginError": "Er is een fout opgetreden tijdens het inloggen", + "loginRequiredForDevice": "Inloggen is vereist om je apparaat te verifiëren.", "passwordForgot": "Wachtwoord vergeten?", "otpAuth": "Tweestapsverificatie verificatie", "otpAuthDescription": "Voer de code van je authenticator-app of een van je reservekopiecodes voor het eenmalig gebruik in.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Startpagina", "sidebarSites": "Werkruimtes", "sidebarResources": "Bronnen", + "sidebarProxyResources": "Openbaar", + "sidebarClientResources": "Privé", "sidebarAccessControl": "Toegangs controle", + "sidebarLogsAndAnalytics": "Logs & Analytics", "sidebarUsers": "Gebruikers", + "sidebarAdmin": "Beheerder", "sidebarInvitations": "Uitnodigingen", "sidebarRoles": "Rollen", - "sidebarShareableLinks": "Deelbare links", + "sidebarShareableLinks": "Koppelingen", "sidebarApiKeys": "API sleutels", "sidebarSettings": "Instellingen", "sidebarAllUsers": "Alle gebruikers", "sidebarIdentityProviders": "Identiteit aanbieders", "sidebarLicense": "Licentie", "sidebarClients": "Clienten", + "sidebarUserDevices": "Gebruikers", + "sidebarMachineClients": "Machines", "sidebarDomains": "Domeinen", + "sidebarGeneral": "Algemeen", + "sidebarLogAndAnalytics": "Log & Analytics", "sidebarBluePrints": "Blauwdrukken", + "sidebarOrganization": "Organisatie", + "sidebarLogsAnalytics": "Analyses", "blueprints": "Blauwdrukken", "blueprintsDescription": "Gebruik declaratieve configuraties en bekijk vorige uitvoeringen.", "blueprintAdd": "Blauwdruk toevoegen", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Bekijk het resultaat van de toegepaste blauwdruk en eventuele fouten", "blueprintInfo": "Blauwdruk Informatie", "message": "bericht", - "blueprintContentsDescription": "Definieer de YAML content die je infrastructuur beschrijft", + "blueprintContentsDescription": "Definieer de YAML-content die de infrastructuur beschrijft", "blueprintErrorCreateDescription": "Er is een fout opgetreden bij het toepassen van de blauwdruk", "blueprintErrorCreate": "Fout bij maken blauwdruk", "searchBlueprintProgress": "Blauwdrukken zoeken...", @@ -1230,15 +1265,15 @@ "loading": "Bezig met laden", "restart": "Herstarten", "domains": "Domeinen", - "domainsDescription": "Beheer domeinen voor je organisatie", + "domainsDescription": "Maak en beheer domeinen die beschikbaar zijn in de organisatie", "domainsSearch": "Zoek domeinen...", "domainAdd": "Domein toevoegen", - "domainAddDescription": "Registreer een nieuw domein bij je organisatie", + "domainAddDescription": "Registreer een nieuw domein met de organisatie", "domainCreate": "Domein aanmaken", "domainCreatedDescription": "Domein succesvol aangemaakt", "domainDeletedDescription": "Domein succesvol verwijderd", - "domainQuestionRemove": "Weet u zeker dat u het domein uit uw account wilt verwijderen?", - "domainMessageRemove": "Na verwijdering zal het domein niet langer aan je account gekoppeld zijn.", + "domainQuestionRemove": "Weet u zeker dat u dit domein wilt verwijderen?", + "domainMessageRemove": "Eenmaal verwijderd, wordt het domein niet langer gekoppeld aan de organisatie.", "domainConfirmDelete": "Bevestig verwijdering van domein", "domainDelete": "Domein verwijderen", "domain": "Domein", @@ -1257,7 +1292,7 @@ "pending": "In afwachting", "sidebarBilling": "Facturering", "billing": "Facturering", - "orgBillingDescription": "Beheer je factureringsgegevens en abonnementen", + "orgBillingDescription": "Beheer factureringsinformatie en abonnementen", "github": "GitHub", "pangolinHosted": "Pangolin gehost", "fossorial": "Fossorial", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Update Producten", "productUpdateEmpty": "Geen updates", "dismissAll": "Alles afwijzen", - "pangolinUpdateAvailable": "Nieuwe versie beschikbaar", + "pangolinUpdateAvailable": "Update beschikbaar", "pangolinUpdateAvailableInfo": "Versie {version} is klaar om te installeren", - "pangolinUpdateAvailableReleaseNotes": "Bekijk release notities", + "pangolinUpdateAvailableReleaseNotes": "Uitgaveopmerkingen bekijken", "newtUpdateAvailable": "Update beschikbaar", "newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.", "domainPickerEnterDomain": "Domein", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Beschikbaarheid controleren...", - "domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden. Probeer een ander domein of controleer de domeininstellingen van uw organisatie.", + "domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden. Probeer een ander domein of controleer de domeininstellingen van de organisatie.", "domainPickerOrganizationDomains": "Organisatiedomeinen", "domainPickerProvidedDomains": "Aangeboden domeinen", "domainPickerSubdomain": "Subdomein: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Abonnementsaanpassing", "billingStartSubscription": "Abonnement Starten", "billingRecurringCharge": "Terugkerende Kosten", - "billingManageSubscriptionSettings": "Beheer uw abonnementsinstellingen en voorkeuren", + "billingManageSubscriptionSettings": "Beheer abonnementsinstellingen en voorkeuren", "billingNoActiveSubscription": "U heeft geen actief abonnement. Start uw abonnement om gebruikslimieten te verhogen.", "billingFailedToLoadSubscription": "Fout bij laden van abonnement", "billingFailedToLoadUsage": "Niet gelukt om gebruik te laden", @@ -1345,9 +1380,9 @@ "billingPortalError": "Portal Fout", "billingDataUsageInfo": "U bent in rekening gebracht voor alle gegevens die via uw beveiligde tunnels via de cloud worden verzonden. Dit omvat zowel inkomende als uitgaande verkeer over al uw sites. Wanneer u uw limiet bereikt zullen uw sites de verbinding verbreken totdat u uw abonnement upgradet of het gebruik vermindert. Gegevens worden niet in rekening gebracht bij het gebruik van knooppunten.", "billingOnlineTimeInfo": "U wordt in rekening gebracht op basis van hoe lang uw sites verbonden blijven met de cloud. Bijvoorbeeld 44,640 minuten is gelijk aan één site met 24/7 voor een volledige maand. Wanneer u uw limiet bereikt, zal de verbinding tussen uw sites worden verbroken totdat u een upgrade van uw abonnement uitvoert of het gebruik vermindert. Tijd wordt niet belast bij het gebruik van knooppunten.", - "billingUsersInfo": "U wordt gefactureerd voor elke gebruiker in uw organisatie. Facturering wordt dagelijks berekend op basis van het aantal actieve gebruikersaccounts in uw organisatie.", - "billingDomainInfo": "U wordt gefactureerd voor elk domein in uw organisatie. Facturering wordt dagelijks berekend op basis van het aantal actieve domeinaccounts in uw organisatie.", - "billingRemoteExitNodesInfo": "U wordt gefactureerd voor elke beheerde Node in uw organisatie. Facturering wordt dagelijks berekend op basis van het aantal actieve beheerde Nodes in uw organisatie.", + "billingUsersInfo": "U bent in rekening gebracht voor elke gebruiker in de organisatie. Facturering wordt dagelijks berekend op basis van het aantal actieve gebruikersaccounts in uw org.", + "billingDomainInfo": "U wordt voor elk domein in de organisatie in rekening gebracht. Facturering wordt dagelijks berekend op basis van het aantal actieve domeinaccounts in uw org.", + "billingRemoteExitNodesInfo": "U bent belast voor elke beheerde node in de organisatie. Facturering wordt dagelijks berekend op basis van het aantal actieve beheerde knooppunten in uw org.", "domainNotFound": "Domein niet gevonden", "domainNotFoundDescription": "Deze bron is uitgeschakeld omdat het domein niet langer in ons systeem bestaat. Stel een nieuw domein in voor deze bron.", "failed": "Mislukt", @@ -1430,29 +1465,32 @@ "and": "en", "privacyPolicy": "privacybeleid" }, + "signUpMarketing": { + "keepMeInTheLoop": "Houd me op de hoogte met nieuws, updates en nieuwe functies per e-mail." + }, "siteRequired": "Site is vereist.", "olmTunnel": "Olm Tunnel", "olmTunnelDescription": "Gebruik Olm voor clientconnectiviteit", "errorCreatingClient": "Fout bij het aanmaken van de client", "clientDefaultsNotFound": "Standaardinstellingen van klant niet gevonden", "createClient": "Client aanmaken", - "createClientDescription": "Maak een nieuwe client aan om verbinding te maken met uw sites", + "createClientDescription": "Maak een nieuwe client aan voor toegang tot privébronnen", "seeAllClients": "Alle clients bekijken", "clientInformation": "Klantinformatie", "clientNamePlaceholder": "Clientnaam", "address": "Adres", "subnetPlaceholder": "Subnet", - "addressDescription": "Het adres dat deze client zal gebruiken voor connectiviteit", + "addressDescription": "Het interne adres van de klant. Moet binnen het subnetwerk van de organisatie vallen.", "selectSites": "Selecteer sites", "sitesDescription": "De client heeft connectiviteit met de geselecteerde sites", "clientInstallOlm": "Installeer Olm", "clientInstallOlmDescription": "Laat Olm draaien op uw systeem", - "clientOlmCredentials": "Olm inloggegevens", - "clientOlmCredentialsDescription": "Dit is hoe Olm zich bij de server zal verifiëren", - "olmEndpoint": "Olm Eindpunt", - "olmId": "Olm ID", - "olmSecretKey": "Olm Geheime Sleutel", - "clientCredentialsSave": "Uw referenties opslaan", + "clientOlmCredentials": "Aanmeldgegevens", + "clientOlmCredentialsDescription": "Dit is hoe de client zich zal verifiëren met de server", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Geheim", + "clientCredentialsSave": "Sla de aanmeldgegevens op", "clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.", "generalSettingsDescription": "Configureer de algemene instellingen voor deze client", "clientUpdated": "Klant bijgewerkt ", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Er is een fout opgetreden bij het ophalen van sites.", "olmErrorFetchReleases": "Er is een fout opgetreden bij het ophalen van Olm releases.", "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 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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Controleer de gezondheid van dit doel. U kunt een ander eindpunt monitoren dan het doel indien vereist.", "healthScheme": "Methode", "healthSelectScheme": "Selecteer methode", + "healthCheckPortInvalid": "Health check poort moet tussen 1 en 65535 zijn", "healthCheckPath": "Pad", "healthHostname": "IP / Hostnaam", "healthPort": "Poort", "healthCheckPathDescription": "Het pad om de gezondheid status te controleren.", - "healthyIntervalSeconds": "Gezonde Interval", - "unhealthyIntervalSeconds": "Ongezonde Interval", + "healthyIntervalSeconds": "Gezonde Interval (sec)", + "unhealthyIntervalSeconds": "Ongezonde Interval (sec)", "IntervalSeconds": "Gezonde Interval", - "timeoutSeconds": "Timeout", + "timeoutSeconds": "Timeout (sec)", "timeIsInSeconds": "Tijd is in seconden", "retryAttempts": "Herhaal Pogingen", "expectedResponseCodes": "Verwachte Reactiecodes", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Domein bewerken", "siteName": "Site Naam", "proxyPort": "Poort", - "resourcesTableProxyResources": "Proxybronnen", - "resourcesTableClientResources": "Clientbronnen", + "resourcesTableProxyResources": "Openbaar", + "resourcesTableClientResources": "Privé", "resourcesTableNoProxyResourcesFound": "Geen proxybronnen gevonden.", "resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.", "resourcesTableDestination": "Bestemming", - "resourcesTableTheseResourcesForUseWith": "Deze bronnen zijn bedoeld voor gebruik met", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Clienten", "resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.", "resourcesTableNoTargets": "Geen doelen", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Offline", "resourcesTableUnknown": "onbekend", "resourcesTableNotMonitored": "Niet gecontroleerd", - "editInternalResourceDialogEditClientResource": "Bewerk clientbron", - "editInternalResourceDialogUpdateResourceProperties": "Werk de eigenschapen van de bron en doelconfiguratie bij voor {resourceName}.", + "editInternalResourceDialogEditClientResource": "Privépagina bewerken", + "editInternalResourceDialogUpdateResourceProperties": "Update de resource configuratie en access control voor {resourceName}", "editInternalResourceDialogResourceProperties": "Bron eigenschappen", "editInternalResourceDialogName": "Naam", "editInternalResourceDialogProtocol": "Protocol", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat", "editInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn", "editInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn", + "editInternalResourceDialogPortModeRequired": "Protocol, proxy poort en bestemmingspoort zijn vereist voor poortmodus", + "editInternalResourceDialogMode": "Modus", + "editInternalResourceDialogModePort": "Poort", + "editInternalResourceDialogModeHost": "Hostnaam", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Bestemming", + "editInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.", + "editInternalResourceDialogDestinationIPDescription": "Het IP of hostnaam adres van de bron op het netwerk van de site.", + "editInternalResourceDialogDestinationCidrDescription": "Het CIDR-bereik van het document op het netwerk van de site.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Een optionele interne DNS-alias voor dit document.", "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.", + "createInternalResourceDialogCreateClientResource": "Privé bron maken", + "createInternalResourceDialogCreateClientResourceDescription": "Maak een nieuwe bron aan die alleen toegankelijk is voor klanten die verbonden zijn met de organisatie", "createInternalResourceDialogResourceProperties": "Bron-eigenschappen", "createInternalResourceDialogName": "Naam", "createInternalResourceDialogSite": "Site", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat", "createInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn", "createInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn", + "createInternalResourceDialogPortModeRequired": "Protocol, proxy poort en bestemmingspoort zijn vereist voor poortmodus", + "createInternalResourceDialogMode": "Modus", + "createInternalResourceDialogModePort": "Poort", + "createInternalResourceDialogModeHost": "Hostnaam", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Bestemming", + "createInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.", + "createInternalResourceDialogDestinationCidrDescription": "Het CIDR-bereik van het document op het netwerk van de site.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Een optionele interne DNS-alias voor dit document.", "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.", + "siteAcceptClientConnectionsDescription": "Sta gebruikersapparaten en clients toegang toe tot bronnen op deze site. Dit kan later worden gewijzigd.", + "siteAddress": "Website Adres (Geavanceerd)", + "siteAddressDescription": "Het interne adres van de site. Moet binnen het subnetwerk van de organisatie vallen.", + "siteNameDescription": "De weergavenaam van de site die later gewijzigd kan worden.", "autoLoginExternalIdp": "Auto Login met Externe IDP", "autoLoginExternalIdpDescription": "De gebruiker onmiddellijk doorsturen naar de externe IDP voor authenticatie.", "selectIdp": "Selecteer IDP", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.", "autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.", "remoteExitNodeManageRemoteExitNodes": "Externe knooppunten", - "remoteExitNodeDescription": "Zorgt voor één of meer externe knooppunten om de netwerkverbinding uit te breiden en het vertrouwen in de cloud te verminderen", + "remoteExitNodeDescription": "Zelf host één of meer externe knooppunten om de netwerkverbinding uit te breiden en het vertrouwen in de cloud te verminderen", "remoteExitNodes": "Nodes", "searchRemoteExitNodes": "Knooppunten zoeken...", "remoteExitNodeAdd": "Voeg node toe", @@ -1623,11 +1682,11 @@ "sidebarRemoteExitNodes": "Externe knooppunten", "remoteExitNodeCreate": { "title": "Maak node", - "description": "Maak een nieuwe node aan om uw netwerkverbinding uit te breiden", + "description": "Maak een nieuwe node aan om de netwerkverbinding uit te breiden", "viewAllButton": "Alle nodes weergeven", "strategy": { "title": "Creatie Strategie", - "description": "Kies dit om uw node handmatig te configureren of nieuwe referenties te genereren.", + "description": "Kies dit om handmatig het knooppunt te configureren of nieuwe referenties te genereren.", "adopt": { "title": "Adopteer Node", "description": "Kies dit als u al de referenties voor deze node heeft" @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Gegeneerde Inloggegevens", - "description": "Gebruik deze gegenereerde inloggegevens om uw node te configureren", + "description": "Gebruik deze gegenereerde inloggegevens om het knooppunt te configureren", "nodeIdTitle": "Knooppunt ID", "secretTitle": "Geheim", "saveCredentialsTitle": "Voeg Inloggegevens toe aan Config", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Identiteit provider type", "roleMappingExpressionPlaceholder": "bijvoorbeeld bevat (groepen, 'admin') && 'Admin' ½ 'Member'", "idpGoogleConfiguration": "Google Configuratie", - "idpGoogleConfigurationDescription": "Configureer uw Google OAuth2-referenties", - "idpGoogleClientIdDescription": "Uw Google OAuth2-client-ID", - "idpGoogleClientSecretDescription": "Uw Google OAuth2 Clientgeheim", + "idpGoogleConfigurationDescription": "Configureer de Google OAuth2-referenties", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Google OAuth2 Clientgeheim", "idpAzureConfiguration": "Azure Entra ID configuratie", - "idpAzureConfigurationDescription": "Configureer uw Azure Entra ID OAuth2 referenties", + "idpAzureConfigurationDescription": "Azure Entra ID OAuth2 referenties configureren", "idpTenantId": "Tenant-ID", - "idpTenantIdPlaceholder": "jouw-tenant-id", - "idpAzureTenantIdDescription": "Uw Azure tenant ID (gevonden in Azure Active Directory overzicht)", - "idpAzureClientIdDescription": "Uw Azure App registratie Client ID", - "idpAzureClientSecretDescription": "Uw Azure App registratie Client Secret", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "Azure tenant ID (gevonden in Azure Active Directory overzicht)", + "idpAzureClientIdDescription": "Azure App registratie Client ID", + "idpAzureClientSecretDescription": "Azure App registratie client geheim", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Google Configuratie", "idpAzureConfigurationTitle": "Azure Entra ID configuratie", "idpTenantIdLabel": "Tenant-ID", - "idpAzureClientIdDescription2": "Uw Azure App registratie Client ID", - "idpAzureClientSecretDescription2": "Uw Azure App registratie Client Secret", + "idpAzureClientIdDescription2": "Azure App registratie Client ID", + "idpAzureClientSecretDescription2": "Azure App registratie client geheim", "idpGoogleDescription": "Google OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Subnet", "subnetDescription": "Het subnet van de netwerkconfiguratie van deze organisatie.", "authPage": "Authenticatie pagina", - "authPageDescription": "De autorisatiepagina voor uw organisatie configureren", + "authPageDescription": "De autorisatiepagina voor de organisatie configureren", "authPageDomain": "Authenticatie pagina domein", "noDomainSet": "Geen domein ingesteld", "changeDomain": "Domein wijzigen", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Authenticatiepagina domein instellen", "failedToFetchCertificate": "Certificaat ophalen mislukt", "failedToRestartCertificate": "Kon certificaat niet opnieuw opstarten", - "addDomainToEnableCustomAuthPages": "Voeg een domein toe om aangepaste authenticatiepagina's voor uw organisatie in te schakelen", + "addDomainToEnableCustomAuthPages": "Een domein toevoegen om aangepaste authenticatiepagina's voor de organisatie in te schakelen", "selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie", "domainPickerProvidedDomain": "Opgegeven domein", "domainPickerFreeProvidedDomain": "Gratis verstrekt domein", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" kon niet geldig worden gemaakt voor {domain}.", "domainPickerSubdomainSanitized": "Subdomein gesaniseerd", "domainPickerSubdomainCorrected": "\"{sub}\" was gecorrigeerd op \"{sanitized}\"", - "orgAuthSignInTitle": "Meld je aan bij je organisatie", + "orgAuthSignInTitle": "Log in op de organisatie", "orgAuthChooseIdpDescription": "Kies uw identiteitsprovider om door te gaan", "orgAuthNoIdpConfigured": "Deze organisatie heeft geen identiteitsproviders geconfigureerd. Je kunt in plaats daarvan inloggen met je Pangolin-identiteit.", "orgAuthSignInWithPangolin": "Log in met Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Tweestapsverificatie inschakelen", "completeSecuritySteps": "Voltooi beveiligingsstappen", "securitySettings": "Beveiliging instellingen", - "securitySettingsDescription": "Beveiligingsbeleid voor uw organisatie configureren", + "securitySettingsDescription": "Beveiligingsbeleid voor de organisatie configureren", "requireTwoFactorForAllUsers": "Authenticatie in twee stappen vereist voor alle gebruikers", "requireTwoFactorDescription": "Wanneer ingeschakeld, moeten alle interne gebruikers in deze organisatie tweestapsverificatie ingeschakeld hebben om toegang te krijgen tot de organisatie.", "requireTwoFactorDisabledDescription": "Deze functie vereist een geldig licentie (Enterprise) of actief abonnement (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Enterprise Edition", "unlicensed": "Ongelicentieerd", "beta": "Bèta", - "manageClients": "Beheer Cliënten", - "manageClientsDescription": "Klanten zijn apparaten die verbinding kunnen maken met uw sites", + "manageUserDevices": "Gebruiker Apparaten", + "manageUserDevicesDescription": "Bekijk en beheer apparaten die gebruikers gebruiken om privé verbinding te maken met bronnen", + "manageMachineClients": "Beheer Machine Clients", + "manageMachineClientsDescription": "Creëer en beheer clients die servers en systemen gebruiken om privé verbinding te maken met bronnen", + "clientsTableUserClients": "Gebruiker", + "clientsTableMachineClients": "Machine", "licenseTableValidUntil": "Geldig tot", "saasLicenseKeysSettingsTitle": "Enterprise Licenties", "saasLicenseKeysSettingsDescription": "Genereer en beheer de Enterprise licentiesleutels voor zelfgehoste Pangolin instanties", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "strip", "sidebarEnableEnterpriseLicense": "Activeer Enterprise Licentie", "cannotbeUndone": "Dit kan niet ongedaan worden gemaakt.", - "toConfirm": "om te bevestigen", + "toConfirm": "om te bevestigen.", "deleteClientQuestion": "Weet u zeker dat u de client van de site en organisatie wilt verwijderen?", "clientMessageRemove": "Eenmaal verwijderd, kan de client geen verbinding meer maken met de site.", "sidebarLogs": "Logboeken", "request": "Aanvragen", + "requests": "Verzoeken", "logs": "Logboeken", "logsSettingsDescription": "Monitor logs verzameld van deze orginiatie", "searchLogs": "Logboeken zoeken...", @@ -2020,6 +2084,7 @@ "ip": "IP-adres", "reason": "Reden", "requestLogs": "Logboeken aanvragen", + "requestAnalytics": "Analytics opvragen", "host": "Hostnaam", "location": "Locatie", "actionLogs": "Actie logs", @@ -2029,6 +2094,7 @@ "logRetention": "Log bewaring", "logRetentionDescription": "Beheren hoe lang verschillende soorten logs bewaard worden voor deze organisatie of schakel ze uit", "requestLogsDescription": "Bekijk gedetailleerde verzoeklogboeken voor resources in deze organisatie", + "requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie", "logRetentionRequestLabel": "Logboekbewaring aanvragen", "logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden", "logRetentionAccessLabel": "Toegang logboek bewaring", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 dagen", "logRetention90Days": "90 dagen", "logRetentionForever": "Voor altijd", + "logRetentionEndOfFollowingYear": "Einde van volgend jaar", "actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie", "accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken", "licenseRequiredToUse": "Een Enterprise-licentie is vereist om deze functie te gebruiken.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Bij voorkeur Wildcard Certificaat", "unverified": "Ongeverifieerd", "domainSetting": "Domein instellingen", - "domainSettingDescription": "Configureer instellingen voor uw domein", + "domainSettingDescription": "Configureer instellingen voor het domein", "preferWildcardCertDescription": "Poging om een certificaat met een wildcard te genereren (vereist een correct geconfigureerde certificaatresolver).", "recordName": "Record Naam", "auto": "Automatisch", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Er is een bijgewerkte versie van Olm beschikbaar. Update alstublieft naar de nieuwste versie voor de beste ervaring.", "client": "Klant", "proxyProtocol": "Proxy Protocol Instellingen", - "proxyProtocolDescription": "Proxyprotocol configureren om de IP-adressen van de client voor TCP/UDP-diensten te bewaren.", + "proxyProtocolDescription": "Proxyprotocol configureren om de IP-adressen van de client voor TCP-diensten te bewaren.", "enableProxyProtocol": "Proxy Protocol inschakelen", - "proxyProtocolInfo": "Behoud IP adressen van de client voor TCP/UDP backends", + "proxyProtocolInfo": "Behoud IP adressen van de client voor TCP backends", "proxyProtocolVersion": "Proxy Protocol Versie", "version1": " Versie 1 (Aanbevolen)", "version2": "Versie 2", "versionDescription": "Versie 1 is text-based en breed ondersteund. Versie 2 is binair en efficiënter maar minder compatibel.", "warning": "Waarschuwing", - "proxyProtocolWarning": "Je backend applicatie moet worden geconfigureerd om connecties met Proxy Protocol te accepteren. Als je backend geen Proxy Protocol ondersteunt, zal het inschakelen van dit alle verbindingen verbreken. Zorg ervoor dat je je backend configureert om Proxy Protocol headers van Traefik.", + "proxyProtocolWarning": "De backend applicatie moet worden geconfigureerd om Proxy Protocol verbindingen te accepteren. Als je backend geen Proxy Protocol ondersteunt, zal het inschakelen van dit alle verbindingen verbreken, dus schakel dit alleen in als je weet wat je doet. Zorg ervoor dat je je backend configureert om Proxy Protocol headers van Traefik.", "restarting": "Herstarten...", "manual": "Handleiding", "messageSupport": "Bericht ondersteuning", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Bericht verzonden!", "supportWillContact": "We nemen binnenkort contact met u op!", "selectLogRetention": "Selecteer log retentie", + "terms": "Voorwaarden", + "privacy": "Privacy", + "security": "Beveiliging", + "docs": "Documentatie", + "deviceActivation": "Apparaat activatie", + "deviceCodeInvalidFormat": "Code moet 9 tekens bevatten (bijv. A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Ongeldige of verlopen code", + "deviceCodeVerifyFailed": "Apparaatcode verifiëren mislukt", + "signedInAs": "Ingelogd als", + "deviceCodeEnterPrompt": "Voer de op het apparaat weergegeven code in", + "continue": "Doorgaan", + "deviceUnknownLocation": "Onbekende locatie", + "deviceAuthorizationRequested": "Deze autorisatie is aangevraagd bij {location} op {date}. Vertrouw je dit apparaat omdat het toegang tot het account zal krijgen.", + "deviceLabel": "Apparaat: {deviceName}", + "deviceWantsAccess": "wil toegang tot je account", + "deviceExistingAccess": "Bestaande toegang:", + "deviceFullAccess": "Volledige toegang tot uw account", + "deviceOrganizationsAccess": "Toegang tot alle organisaties waar uw account toegang tot heeft", + "deviceAuthorize": "Autoriseer {applicationName}", + "deviceConnected": "Apparaat verbonden!", + "deviceAuthorizedMessage": "Apparaat is gemachtigd om toegang te krijgen tot je account.", + "pangolinCloud": "Pangoline Cloud", + "viewDevices": "Bekijk apparaten", + "viewDevicesDescription": "Beheer uw aangesloten apparaten", + "noDevices": "Geen apparaten gevonden", + "dateCreated": "Datum aangemaakt", + "unnamedDevice": "Naamloos apparaat", + "deviceQuestionRemove": "Weet u zeker dat u dit apparaat wilt verwijderen?", + "deviceMessageRemove": "Deze actie kan niet ongedaan worden gemaakt.", + "deviceDeleteConfirm": "Apparaat verwijderen", + "deleteDevice": "Apparaat verwijderen", + "errorLoadingDevices": "Fout bij laden van apparaten", + "failedToLoadDevices": "Fout bij het laden van apparaten", + "deviceDeleted": "Apparaat verwijderd", + "deviceDeletedDescription": "Het apparaat is succesvol verwijderd.", + "errorDeletingDevice": "Fout bij verwijderen apparaat", + "failedToDeleteDevice": "Verwijderen van apparaat mislukt", "showColumns": "Kolommen weergeven", "hideColumns": "Kolommen verbergen", "columnVisibility": "Zichtbaarheid kolommen", @@ -2111,10 +2215,14 @@ "enableSelected": "Selectie inschakelen", "disableSelected": "Selectie uitschakelen", "checkSelectedStatus": "Controleer de status van de geselecteerde", + "clients": "Clienten", + "accessClientSelect": "Selecteer machine-clients", + "resourceClientDescription": "Machine clients die toegang hebben tot deze bron", + "regenerate": "Hergenereren", "credentials": "Aanmeldgegevens", "savecredentials": "Referenties opslaan", - "regeneratecredentials": "Hersleutel", - "regenerateCredentials": "Opnieuw genereren en opslaan van uw referenties", + "regenerateCredentialsButton": "Referenties opnieuw genereren", + "regenerateCredentials": "Referenties opnieuw genereren", "generatedcredentials": "Gegenereerde referenties", "copyandsavethesecredentials": "Kopieer en bewaar deze inloggegevens", "copyandsavethesecredentialsdescription": "Deze referenties worden niet meer getoond nadat u deze pagina verlaat. Sla ze nu veilig op.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Referenties werden met succes opnieuw gegenereerd en opgeslagen.", "credentialsSaveError": "Fout bij opslaan referenties", "credentialsSaveErrorDescription": "Er is een fout opgetreden tijdens het opnieuw genereren en opslaan van de inloggegevens.", - "regenerateCredentialsWarning": "Het opnieuw genereren van inloggegevens zal de vorige ongeldig maken. Zorg ervoor dat alle configuraties die deze inloggegevens gebruiken bijgewerkt worden.", + "regenerateCredentialsWarning": "Het opnieuw genereren van inloggegevens zal de vorige ongeldig maken en een slechte verbinding veroorzaken. Zorg ervoor dat u alle configuraties die deze inloggegevens gebruiken bijwerkt.", "confirm": "Bevestigen", "regenerateCredentialsConfirmation": "Weet u zeker dat u de inloggegevens opnieuw wilt genereren?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Geheime sleutel", - "featureDisabledTooltip": "Deze functie is alleen beschikbaar in het bedrijfsplan en vereist een licentie om deze te gebruiken.", "niceId": "Leuk ID", "niceIdUpdated": "Leuke ID bijgewerkt", "niceIdUpdatedSuccessfully": "Nice ID Updated Successfully", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Fout opgetreden tijdens het bijwerken van de ID van Nice.", "niceIdCannotBeEmpty": "Nice ID mag niet leeg zijn", "enterIdentifier": "ID invoeren", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "Niet u? Gebruik een ander account.", + "deviceLoginDeviceRequestingAccessToAccount": "Een apparaat vraagt om toegang tot dit account.", + "noData": "Geen gegevens", + "machineClients": "Machine Clienten", + "install": "Installeren", + "run": "Uitvoeren", + "clientNameDescription": "De weergavenaam van de client die later gewijzigd kan worden.", + "clientAddress": "Klant adres (Geavanceerd)", + "setupFailedToFetchSubnet": "Kan standaard subnet niet ophalen", + "setupSubnetAdvanced": "Subnet (Geavanceerd)", + "setupSubnetDescription": "Het subnet van het interne netwerk van deze organisatie.", + "siteRegenerateAndDisconnect": "Hergenereer en verbreek verbinding", + "siteRegenerateAndDisconnectConfirmation": "Weet u zeker dat u de inloggegevens opnieuw wilt genereren en de verbinding met deze website wilt verbreken?", + "siteRegenerateAndDisconnectWarning": "Dit zal de inloggegevens opnieuw genereren en onmiddellijk de site ontkoppelen. De site zal opnieuw moeten worden gestart met de nieuwe inloggegevens.", + "siteRegenerateCredentialsConfirmation": "Weet u zeker dat u de referenties voor deze site opnieuw wilt genereren?", + "siteRegenerateCredentialsWarning": "Dit zal de inloggegevens opnieuw genereren. De site zal verbonden blijven totdat u het handmatig herstart en de nieuwe inloggegevens gebruikt.", + "clientRegenerateAndDisconnect": "Hergenereer en verbreek verbinding", + "clientRegenerateAndDisconnectConfirmation": "Weet u zeker dat u de inloggegevens opnieuw wilt genereren en de verbinding met deze client wilt verbreken?", + "clientRegenerateAndDisconnectWarning": "Dit zal de inloggegevens opnieuw genereren en onmiddellijk de verbinding verbreken. De client zal opnieuw moeten worden gestart met de nieuwe inloggegevens.", + "clientRegenerateCredentialsConfirmation": "Weet u zeker dat u de referenties voor deze client opnieuw wilt genereren?", + "clientRegenerateCredentialsWarning": "Dit zal de inloggegevens opnieuw genereren. De client zal verbonden blijven totdat u het handmatig herstart en de nieuwe inloggegevens gebruikt.", + "remoteExitNodeRegenerateAndDisconnect": "Hergenereer en verbreek verbinding", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Weet u zeker dat u de inloggegevens opnieuw wilt genereren en deze afstandsbediening wilt loskoppelen?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Dit zal de referenties regenereren en onmiddellijk de externe exit node ontkoppelen. Het externe exit node zal opnieuw moeten worden gestart met de nieuwe referenties.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Weet u zeker dat u de referenties voor deze externe exit node opnieuw wilt genereren?", + "remoteExitNodeRegenerateCredentialsWarning": "Dit zal de referenties opnieuw genereren. De remote exit node zal verbonden blijven totdat u deze handmatig herstart en de nieuwe referenties gebruikt.", + "agent": "Agent" } diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 6c401dfa..99817d14 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1,12 +1,12 @@ { - "setupCreate": "Utwórz swoją organizację, witrynę i zasoby", + "setupCreate": "Utwórz organizację, witrynę i zasoby", "setupNewOrg": "Nowa organizacja", "setupCreateOrg": "Utwórz organizację", "setupCreateResources": "Utwórz Zasoby", "setupOrgName": "Nazwa organizacji", - "orgDisplayName": "To jest wyświetlana nazwa Twojej organizacji.", + "orgDisplayName": "To jest wyświetlana nazwa organizacji.", "orgId": "Identyfikator organizacji", - "setupIdentifierMessage": "To jest unikalny identyfikator Twojej organizacji. Jest to oddzielone od nazwy wyświetlanej.", + "setupIdentifierMessage": "Jest to unikalny identyfikator organizacji.", "setupErrorIdentifier": "Identyfikator organizacji jest już zajęty. Wybierz inny.", "componentsErrorNoMemberCreate": "Nie jesteś obecnie członkiem żadnej organizacji. Aby rozpocząć, utwórz organizację.", "componentsErrorNoMember": "Nie jesteś obecnie członkiem żadnej organizacji.", @@ -50,7 +50,7 @@ "siteMessageRemove": "Po usunięciu witryna nie będzie już dostępna. Wszystkie cele związane z witryną zostaną również usunięte.", "siteQuestionRemove": "Czy na pewno chcesz usunąć witrynę z organizacji?", "siteManageSites": "Zarządzaj stronami", - "siteDescription": "Zezwalaj na połączenie z siecią przez bezpieczne tunele", + "siteDescription": "Tworzenie stron i zarządzanie nimi, aby włączyć połączenia z prywatnymi sieciami", "siteCreate": "Utwórz witrynę", "siteCreateDescription2": "Wykonaj poniższe kroki, aby utworzyć i połączyć nową witrynę", "siteCreateDescription": "Utwórz nową witrynę, aby rozpocząć łączenie zasobów", @@ -87,32 +87,32 @@ "siteUpdated": "Strona zaktualizowana", "siteUpdatedDescription": "Strona została zaktualizowana.", "siteGeneralDescription": "Skonfiguruj ustawienia ogólne dla tej witryny", - "siteSettingDescription": "Skonfiguruj ustawienia na swojej stronie", + "siteSettingDescription": "Skonfiguruj ustawienia na stronie", "siteSetting": "Ustawienia {siteName}", - "siteNewtTunnel": "Newt Tunnel (Zalecane)", - "siteNewtTunnelDescription": "Łatwiejszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.", + "siteNewtTunnel": "Newt Site (Rekomendowane)", + "siteNewtTunnelDescription": "Najprostszy sposób na stworzenie punktu wejścia w żadnej sieci. Nie ma dodatkowej konfiguracji.", "siteWg": "Podstawowy WireGuard", "siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.", "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": "Tylko zasoby lokalne. Brak tunelu. Dostępne tylko w węzłach zdalnych.", "siteSeeAll": "Zobacz wszystkie witryny", - "siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną", - "siteNewtCredentials": "Aktualne dane logowania", - "siteNewtCredentialsDescription": "Oto jak Newt będzie uwierzytelniał się z serwerem", - "siteCredentialsSave": "Zapisz swoje poświadczenia", + "siteTunnelDescription": "Określ jak chcesz połączyć się z witryną", + "siteNewtCredentials": "Dane logowania", + "siteNewtCredentialsDescription": "Oto jak witryna będzie uwierzytelniać się z serwerem", + "siteCredentialsSave": "Zapisz dane logowania", "siteCredentialsSaveDescription": "Możesz to zobaczyć tylko raz. Upewnij się, że skopiuj je do bezpiecznego miejsca.", "siteInfo": "Informacje o witrynie", "status": "Status", "shareTitle": "Zarządzaj linkami udostępniania", - "shareDescription": "Utwórz linki, które można udostępnić, aby przyznać tymczasowy lub stały dostęp do Twoich zasobów", + "shareDescription": "Utwórz linki do współdzielenia, aby przyznać tymczasowy lub stały dostęp do zasobów proxy", "shareSearch": "Szukaj linków udostępnienia...", "shareCreate": "Utwórz link udostępniania", "shareErrorDelete": "Nie udało się usunąć linku", "shareErrorDeleteMessage": "Wystąpił błąd podczas usuwania linku", "shareDeleted": "Link usunięty", "shareDeletedDescription": "Link został usunięty", - "shareTokenDescription": "Twój token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.", + "shareTokenDescription": "Token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.", "accessToken": "Token dostępu", "usageExamples": "Przykłady użycia", "tokenId": "Identyfikator tokena", @@ -121,7 +121,7 @@ "importantNote": "Ważna uwaga", "shareImportantDescription": "Ze względów bezpieczeństwa zaleca się użycie nagłówków nad parametrami zapytania, jeśli to możliwe, ponieważ parametry zapytania mogą być zalogowane w dziennikach serwera lub historii przeglądarki.", "token": "Token", - "shareTokenSecurety": "Chroń swój token dostępu. Nie udostępniaj go w publicznie dostępnych miejscach ani w kodzie po stronie klienta.", + "shareTokenSecurety": "Zachowaj bezpieczny token dostępu. Nie udostępniaj go w publicznie dostępnych miejscach ani w kodzie po stronie klienta.", "shareErrorFetchResource": "Nie udało się pobrać zasobów", "shareErrorFetchResourceDescription": "Wystąpił błąd podczas pobierania zasobów", "shareErrorCreate": "Nie udało się utworzyć linku udostępniania", @@ -144,8 +144,10 @@ "expires": "Wygasa", "never": "Nigdy", "shareErrorSelectResource": "Wybierz zasób", - "resourceTitle": "Zarządzaj zasobami", - "resourceDescription": "Utwórz bezpieczne proxy do prywatnych aplikacji", + "proxyResourceTitle": "Zarządzaj zasobami publicznymi", + "proxyResourceDescription": "Twórz i zarządzaj zasobami, które są publicznie dostępne w przeglądarce internetowej", + "clientResourceTitle": "Zarządzaj zasobami prywatnymi", + "clientResourceDescription": "Twórz i zarządzaj zasobami, które są dostępne tylko za pośrednictwem połączonego klienta", "resourcesSearch": "Szukaj zasobów...", "resourceAdd": "Dodaj zasób", "resourceErrorDelte": "Błąd podczas usuwania zasobu", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Po usunięciu, zasób nie będzie już dostępny. Wszystkie cele związane z zasobem zostaną również usunięte.", "resourceQuestionRemove": "Czy na pewno chcesz usunąć zasób z organizacji?", "resourceHTTP": "Zasób HTTPS", - "resourceHTTPDescription": "Proxy do Twojej aplikacji przez HTTPS, przy użyciu poddomeny lub domeny bazowej.", + "resourceHTTPDescription": "Proxy żądania do aplikacji przez HTTPS przy użyciu poddomeny lub domeny bazowej.", "resourceRaw": "Surowy zasób TCP/UDP", - "resourceRawDescription": "Proxy do aplikacji przez TCP/UDP przy użyciu numeru portu.", + "resourceRawDescription": "Proxy żądania do aplikacji przez TCP/UDP przy użyciu numeru portu. Działa to tylko wtedy, gdy witryny są podłączone do węzłów.", "resourceCreate": "Utwórz zasób", "resourceCreateDescription": "Wykonaj poniższe kroki, aby utworzyć nowy zasób", "resourceSeeAll": "Zobacz wszystkie zasoby", @@ -171,22 +173,22 @@ "noCountryFound": "Nie znaleziono kraju.", "siteSelectionDescription": "Ta strona zapewni połączenie z celem.", "resourceType": "Typ zasobu", - "resourceTypeDescription": "Określ jak chcesz uzyskać dostęp do swojego zasobu", + "resourceTypeDescription": "Określ jak uzyskać dostęp do zasobu", "resourceHTTPSSettings": "Ustawienia HTTPS", - "resourceHTTPSSettingsDescription": "Skonfiguruj jak twój zasób będzie dostępny przez HTTPS", + "resourceHTTPSSettingsDescription": "Skonfiguruj jak zasób będzie dostępny przez HTTPS", "domainType": "Typ domeny", "subdomain": "Poddomena", "baseDomain": "Bazowa domena", - "subdomnainDescription": "Poddomena, w której twój zasób będzie dostępny.", + "subdomnainDescription": "Poddomena, w której zasób będzie dostępny.", "resourceRawSettings": "Ustawienia TCP/UDP", - "resourceRawSettingsDescription": "Skonfiguruj jak twój zasób będzie dostępny przez TCP/UDP. Zmapujesz zasób do portu na serwerze hosta Pangolin, dzięki czemu możesz uzyskać dostęp do zasobu z serwera-public ip:mapped-port.", + "resourceRawSettingsDescription": "Skonfiguruj jak zasób będzie dostępny przez TCP/UDP", "protocol": "Protokół", "protocolSelect": "Wybierz protokół", "resourcePortNumber": "Numer portu", "resourcePortNumberDescription": "Numer portu zewnętrznego do żądań proxy.", "cancel": "Anuluj", "resourceConfig": "Snippety konfiguracji", - "resourceConfigDescription": "Skopiuj i wklej te fragmenty konfiguracji, aby skonfigurować swój zasób TCP/UDP", + "resourceConfigDescription": "Skopiuj i wklej te fragmenty konfiguracji, aby skonfigurować zasób TCP/UDP", "resourceAddEntrypoints": "Traefik: Dodaj punkty wejścia", "resourceExposePorts": "Gerbil: Podnieś porty w Komponencie Dockera", "resourceLearnRaw": "Dowiedz się, jak skonfigurować zasoby TCP/UDP", @@ -204,12 +206,12 @@ "rules": "Regulamin", "resourceSettingDescription": "Skonfiguruj ustawienia zasobu", "resourceSetting": "Ustawienia {resourceName}", - "alwaysAllow": "Zawsze zezwalaj", - "alwaysDeny": "Zawsze odmawiaj", + "alwaysAllow": "Omijanie uwierzytelniania", + "alwaysDeny": "Blokuj dostęp", "passToAuth": "Przekaż do Autoryzacji", - "orgSettingsDescription": "Skonfiguruj ustawienia ogólne swojej organizacji", + "orgSettingsDescription": "Skonfiguruj ustawienia organizacji", "orgGeneralSettings": "Ustawienia organizacji", - "orgGeneralSettingsDescription": "Zarządzaj szczegółami swojej organizacji i konfiguracją", + "orgGeneralSettingsDescription": "Zarządzaj szczegółami i konfiguracją organizacji", "saveGeneralSettings": "Zapisz ustawienia ogólne", "saveSettings": "Zapisz ustawienia", "orgDangerZone": "Strefa zagrożenia", @@ -232,7 +234,7 @@ "orgMissing": "Brak ID organizacji", "orgMissingMessage": "Nie można ponownie wygenerować zaproszenia bez ID organizacji.", "accessUsersManage": "Zarządzaj użytkownikami", - "accessUsersDescription": "Zaproś użytkowników i dodaj je do ról do zarządzania dostępem do Twojej organizacji", + "accessUsersDescription": "Zaproś użytkowników z dostępem do tej organizacji i zarządzaj nimi", "accessUsersSearch": "Szukaj użytkowników...", "accessUserCreate": "Utwórz użytkownika", "accessUserRemove": "Usuń użytkownika", @@ -241,13 +243,13 @@ "role": "Rola", "nameRequired": "Nazwa jest wymagana", "accessRolesManage": "Zarządzaj rolami", - "accessRolesDescription": "Skonfiguruj role do zarządzania dostępem do Twojej organizacji", + "accessRolesDescription": "Tworzenie ról dla użytkowników w organizacji i zarządzanie nimi", "accessRolesSearch": "Szukaj ról...", "accessRolesAdd": "Dodaj rolę", "accessRoleDelete": "Usuń rolę", "description": "Opis", "inviteTitle": "Otwórz zaproszenia", - "inviteDescription": "Zarządzaj zaproszeniami dla innych użytkowników", + "inviteDescription": "Zarządzaj zaproszeniami dla innych użytkowników do dołączenia do organizacji", "inviteSearch": "Szukaj zaproszeń...", "minutes": "Protokoły", "hours": "Godziny", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Błąd podczas tworzenia klucza API", "apiKeysErrorSetPermission": "Błąd podczas ustawiania uprawnień", "apiKeysCreate": "Generuj klucz API", - "apiKeysCreateDescription": "Wygeneruj nowy klucz API dla swojej organizacji", + "apiKeysCreateDescription": "Wygeneruj nowy klucz API dla organizacji", "apiKeysGeneralSettings": "Uprawnienia", "apiKeysGeneralSettingsDescription": "Określ, co ten klucz API może zrobić", - "apiKeysList": "Twój klucz API", - "apiKeysSave": "Zapisz swój klucz API", + "apiKeysList": "Nowy klucz API", + "apiKeysSave": "Zapisz klucz API", "apiKeysSaveDescription": "Będziesz mógł zobaczyć to tylko raz. Upewnij się, że skopiujesz go w bezpieczne miejsce.", - "apiKeysInfo": "Twój klucz API to:", + "apiKeysInfo": "Kluczem API jest:", "apiKeysConfirmCopy": "Skopiowałem klucz API", "generate": "Generuj", "done": "Gotowe", @@ -424,7 +426,7 @@ "userCreated": "Utworzono użytkownika", "userCreatedDescription": "Użytkownik został pomyślnie utworzony.", "userTypeInternal": "Użytkownik wewnętrzny", - "userTypeInternalDescription": "Zaproś użytkownika do bezpośredniego dołączenia do Twojej organizacji.", + "userTypeInternalDescription": "Zaproś użytkownika do dołączenia do organizacji bezpośrednio.", "userTypeExternal": "Użytkownik zewnętrzny", "userTypeExternalDescription": "Utwórz użytkownika z zewnętrznym dostawcą tożsamości.", "accessUserCreateDescription": "Wykonaj poniższe kroki, aby utworzyć nowego użytkownika", @@ -436,6 +438,16 @@ "inviteEmailSent": "Wyślij email z zaproszeniem do użytkownika", "inviteValid": "Ważne przez", "selectDuration": "Wybierz okres", + "selectResource": "Wybierz zasób", + "filterByResource": "Filtruj według zasobów", + "resetFilters": "Resetuj filtry", + "totalBlocked": "Żądania zablokowane przez Pangolina", + "totalRequests": "Wszystkich Żądań", + "requestsByCountry": "Żądania według kraju", + "requestsByDay": "Żądania wg dnia", + "blocked": "Zablokowane", + "allowed": "Dozwolone", + "topCountries": "Najlepsze kraje", "accessRoleSelect": "Wybierz rolę", "inviteEmailSentDescription": "Email został wysłany do użytkownika z linkiem dostępu poniżej. Musi on uzyskać dostęp do linku, aby zaakceptować zaproszenie.", "inviteSentDescription": "Użytkownik został zaproszony. Musi uzyskać dostęp do poniższego linku, aby zaakceptować zaproszenie.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Zapisz kontrole dostępu", "roles": "Role", "accessUsersRoles": "Zarządzaj użytkownikami i rolami", - "accessUsersRolesDescription": "Zapraszaj użytkowników i dodawaj ich do ról, aby zarządzać dostępem do Twojej organizacji", + "accessUsersRolesDescription": "Zaproś użytkowników i dodaj je do ról do zarządzania dostępem do organizacji", "key": "Klucz", "createdAt": "Utworzono", "proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.", "proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.", "proxyEnableSSL": "Włącz SSL", - "proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z Twoimi celami.", + "proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z celami.", "target": "Target", "configureTarget": "Konfiguruj Targety", "targetErrorFetch": "Nie udało się pobrać celów", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Nie udało się zaktualizować celów", "targetsErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji celów", "targetTlsUpdate": "Ustawienia TLS zaktualizowane", - "targetTlsUpdateDescription": "Twoje ustawienia TLS zostały pomyślnie zaktualizowane", + "targetTlsUpdateDescription": "Ustawienia TLS zostały pomyślnie zaktualizowane", "targetErrorTlsUpdate": "Nie udało się zaktualizować ustawień TLS", "targetErrorTlsUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień TLS", "proxyUpdated": "Ustawienia proxy zaktualizowane", - "proxyUpdatedDescription": "Twoje ustawienia proxy zostały pomyślnie zaktualizowane", + "proxyUpdatedDescription": "Ustawienia proxy zostały pomyślnie zaktualizowane", "proxyErrorUpdate": "Nie udało się zaktualizować ustawień proxy", "proxyErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień proxy", - "targetAddr": "IP / Nazwa hosta", + "targetAddr": "Host", "targetPort": "Port", "targetProtocol": "Protokół", "targetTlsSettings": "Konfiguracja bezpiecznego połączenia", - "targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu", + "targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla zasobu", "targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS", "targetTlsSni": "Nazwa serwera TLS", "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 usług zaplecza", + "targetsDescription": "Ustaw cele dla ruchu na trasie w celu obsługi zaplecza", "targetStickySessions": "Włącz sesje trwałe", "targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.", "methodSelect": "Wybierz metodę", "targetSubmit": "Dodaj cel", - "targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel, aby skonfigurować miejsce wysyłania żądań do twojego backendu.", + "targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel do skonfigurowania adresów wysyłania żądań do backendu.", "targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.", "targetsSubmit": "Zapisz cele", "addTarget": "Dodaj cel", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Cel został utworzony pomyślnie", "targetErrorCreate": "Nie udało się utworzyć celu", "targetErrorCreateDescription": "Wystąpił błąd podczas tworzenia celu", + "tlsServerName": "Nazwa serwera TLS", + "tlsServerNameDescription": "Nazwa serwera TLS do użycia dla SNI", "save": "Zapisz", "proxyAdditional": "Dodatkowe ustawienia proxy", - "proxyAdditionalDescription": "Skonfiguruj jak twój zasób obsługuje ustawienia proxy", + "proxyAdditionalDescription": "Skonfiguruj sposób obsługi ustawień proxy", "proxyCustomHeader": "Niestandardowy nagłówek hosta", "proxyCustomHeaderDescription": "Nagłówek hosta do ustawienia podczas proxy żądań. Pozostaw puste, aby użyć domyślnego.", "proxyAdditionalSubmit": "Zapisz ustawienia proxy", @@ -558,7 +572,7 @@ "rulesMatchType": "Typ dopasowania", "value": "Wartość", "rulesAbout": "O regułach", - "rulesAboutDescription": "Reguły pozwalają kontrolować dostęp do zasobu na podstawie zestawu kryteriów. Możesz tworzyć reguły zezwalające lub odmawiające dostępu na podstawie adresu IP lub ścieżki URL.", + "rulesAboutDescription": "Reguły pozwalają kontrolować dostęp do zasobu na podstawie zestawu kryteriów. Możesz utworzyć reguły, aby zezwolić lub odmówić dostępu w oparciu o adres IP lub ścieżkę URL.", "rulesActions": "Akcje", "rulesActionAlwaysAllow": "Zawsze zezwalaj: Pomiń wszystkie metody uwierzytelniania", "rulesActionAlwaysDeny": "Zawsze odmawiaj: Blokuj wszystkie żądania; nie można próbować uwierzytelniania", @@ -570,7 +584,7 @@ "rulesEnable": "Włącz reguły", "rulesEnableDescription": "Włącz lub wyłącz ocenę reguł dla tego zasobu", "rulesResource": "Konfiguracja reguł zasobu", - "rulesResourceDescription": "Skonfiguruj reguły kontroli dostępu do zasobu", + "rulesResourceDescription": "Skonfiguruj reguły, aby kontrolować dostęp do zasobu", "ruleSubmit": "Dodaj regułę", "rulesNoOne": "Brak reguł. Dodaj regułę używając formularza.", "rulesOrder": "Reguły są oceniane według priorytetu w kolejności rosnącej.", @@ -586,7 +600,7 @@ "none": "Brak", "unknown": "Nieznany", "resources": "Zasoby", - "resourcesDescription": "Zasoby są proxy do aplikacji działających w Twojej sieci prywatnej. Utwórz zasób dla dowolnej usługi HTTP/HTTPS lub surowej TCP/UDP w Twojej sieci prywatnej. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne, bezpieczne połączenie przez zaszyfrowany tunel WireGuard.", + "resourcesDescription": "Zasoby to proxy do aplikacji działających w sieci prywatnej. Utwórz zasób dla dowolnej usługi HTTP/HTTPS lub surowej usługi TCP/UDP w sieci prywatnej. Każdy zasób musi być podłączony do witryny, aby umożliwić prywatną, bezpieczną łączność przez zaszyfrowany tunel WireGuard.", "resourcesWireGuardConnect": "Bezpieczne połączenie z szyfrowaniem WireGuard", "resourcesMultipleAuthenticationMethods": "Skonfiguruj wiele metod uwierzytelniania", "resourcesUsersRolesAccess": "Kontrola dostępu oparta na użytkownikach i rolach", @@ -597,7 +611,7 @@ "resourceSelect": "Wybierz zasób", "shareLinks": "Linki udostępniania", "share": "Linki do udostępniania", - "shareDescription2": "Twórz linki do udostępniania swoich zasobów. Linki zapewniają tymczasowy lub nieograniczony dostęp do zasobu. Podczas tworzenia linku możesz skonfigurować okres jego ważności.", + "shareDescription2": "Utwórz linki do zasobów, które można współdzielić. Linki zapewniają tymczasowy lub nieograniczony dostęp do twojego zasobu. Możesz skonfigurować czas ważności linku, gdy go utworzysz.", "shareEasyCreate": "Łatwe tworzenie i udostępnianie", "shareConfigurableExpirationDuration": "Konfigurowalny okres ważności", "shareSecureAndRevocable": "Bezpieczne i odwoływalne", @@ -607,19 +621,19 @@ "unknownCommand": "Nieznane polecenie", "newtErrorFetchReleases": "Nie udało się pobrać informacji o wydaniu: {err}", "newtErrorFetchLatest": "Błąd podczas pobierania najnowszego wydania: {err}", - "newtEndpoint": "Punkt końcowy Newt", - "newtId": "ID Newt", - "newtSecretKey": "Klucz tajny Newt", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Sekret", "architecture": "Architektura", "sites": "Witryny", - "siteWgAnyClients": "Użyj dowolnego klienta WireGuard do połączenia. Będziesz musiał adresować swoje zasoby wewnętrzne używając IP peera.", + "siteWgAnyClients": "Użyj dowolnego klienta WireGuard, aby się połączyć. Będziesz musiał przekierować wewnętrzne zasoby za pomocą adresu IP.", "siteWgCompatibleAllClients": "Kompatybilny ze wszystkimi klientami WireGuard", "siteWgManualConfigurationRequired": "Wymagana konfiguracja ręczna", "userErrorNotAdminOrOwner": "Użytkownik nie jest administratorem ani właścicielem", "pangolinSettings": "Ustawienia - Pangolin", "accessRoleYour": "Twoja rola:", - "accessRoleSelect2": "Wybierz rolę", - "accessUserSelect": "Wybierz użytkownika", + "accessRoleSelect2": "Wybierz role", + "accessUserSelect": "Wybierz użytkowników", "otpEmailEnter": "Wprowadź adres e-mail", "otpEmailEnterDescription": "Naciśnij enter, aby dodać adres e-mail po wpisaniu go w polu.", "otpEmailErrorInvalid": "Nieprawidłowy adres e-mail. Znak wieloznaczny (*) musi być całą częścią lokalną.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Ustaw kod PIN", "resourcePincodeSetupTitleDescription": "Ustaw kod PIN, aby chronić ten zasób", "resourceRoleDescription": "Administratorzy zawsze mają dostęp do tego zasobu.", - "resourceUsersRoles": "Użytkownicy i role", + "resourceUsersRoles": "Kontrola dostępu", "resourceUsersRolesDescription": "Skonfiguruj, którzy użytkownicy i role mogą odwiedzać ten zasób", "resourceUsersRolesSubmit": "Zapisz użytkowników i role", "resourceWhitelistSave": "Zapisano pomyślnie", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Przenieś zasób", "siteDestination": "Witryna docelowa", "searchSites": "Szukaj witryn", + "countries": "Kraje", "accessRoleCreate": "Utwórz rolę", "accessRoleCreateDescription": "Utwórz nową rolę aby zgrupować użytkowników i zarządzać ich uprawnieniami.", "accessRoleCreateSubmit": "Utwórz rolę", @@ -766,15 +781,15 @@ "idpOidcConfigure": "Konfiguracja OAuth2/OIDC", "idpOidcConfigureDescription": "Skonfiguruj punkty końcowe i poświadczenia dostawcy OAuth2/OIDC", "idpClientId": "ID klienta", - "idpClientIdDescription": "ID klienta OAuth2 od twojego dostawcy tożsamości", + "idpClientIdDescription": "Identyfikator klienta OAuth2 od dostawcy tożsamości", "idpClientSecret": "Sekret klienta", - "idpClientSecretDescription": "Sekret klienta OAuth2 od twojego dostawcy tożsamości", + "idpClientSecretDescription": "Sekret klienta OAuth2 od dostawcy tożsamości", "idpAuthUrl": "URL autoryzacji", "idpAuthUrlDescription": "URL punktu końcowego autoryzacji OAuth2", "idpTokenUrl": "URL tokena", "idpTokenUrlDescription": "URL punktu końcowego tokena OAuth2", "idpOidcConfigureAlert": "Ważna informacja", - "idpOidcConfigureAlertDescription": "Po utworzeniu dostawcy tożsamości, będziesz musiał skonfigurować URL wywołania zwrotnego w ustawieniach swojego dostawcy tożsamości. URL wywołania zwrotnego zostanie podany po pomyślnym utworzeniu.", + "idpOidcConfigureAlertDescription": "Po utworzeniu dostawcy tożsamości, musisz skonfigurować adres URL wywołania zwrotnego w ustawieniach dostawcy tożsamości. Adres zwrotny zostanie podany po pomyślnym utworzeniu.", "idpToken": "Konfiguracja tokena", "idpTokenDescription": "Skonfiguruj jak wydobywać informacje o użytkowniku z tokena ID", "idpJmespathAbout": "O JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Utwórz dostawcę tożsamości", "orgPolicies": "Polityki organizacji", "idpSettings": "Ustawienia {idpName}", - "idpCreateSettingsDescription": "Skonfiguruj ustawienia dla swojego dostawcy tożsamości", + "idpCreateSettingsDescription": "Skonfiguruj ustawienia dostawcy tożsamości", "roleMapping": "Mapowanie ról", "orgMapping": "Mapowanie organizacji", "orgPoliciesSearch": "Szukaj polityk organizacji...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Dostawca tożsamości został pomyślnie zaktualizowany", "redirectUrl": "URL przekierowania", "redirectUrlAbout": "O URL przekierowania", - "redirectUrlAboutDescription": "Jest to URL, na który użytkownicy zostaną przekierowani po uwierzytelnieniu. Musisz skonfigurować ten URL w ustawieniach swojego dostawcy tożsamości.", + "redirectUrlAboutDescription": "Jest to adres URL, na który użytkownicy zostaną przekierowani po uwierzytelnieniu. Musisz skonfigurować ten adres URL w ustawieniach dostawcy tożsamości.", "pangolinAuth": "Autoryzacja - Pangolin", "verificationCodeLengthRequirements": "Twój kod weryfikacyjny musi mieć 8 znaków.", "errorOccurred": "Wystąpił błąd", @@ -909,6 +924,10 @@ "passwordResetSent": "Wyślemy kod resetowania hasła na ten adres e-mail.", "passwordResetCode": "Kod resetowania", "passwordResetCodeDescription": "Sprawdź swój e-mail, aby znaleźć kod resetowania.", + "generatePasswordResetCode": "Generuj kod resetowania hasła", + "passwordResetCodeGenerated": "Wygenerowany kod resetowania hasła", + "passwordResetCodeGeneratedDescription": "Udostępnij ten kod użytkownikowi. Mogą go użyć do zresetowania hasła.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nowe hasło", "passwordNewConfirm": "Potwierdź nowe hasło", "changePassword": "Zmień hasło", @@ -926,6 +945,9 @@ "pincodeAuth": "Kod uwierzytelniający", "pincodeSubmit2": "Wyślij kod", "passwordResetSubmit": "Zażądaj resetowania", + "passwordResetAlreadyHaveCode": "Wprowadź kod resetowania hasła", + "passwordResetSmtpRequired": "Skontaktuj się z administratorem", + "passwordResetSmtpRequiredDescription": "Aby zresetować hasło, wymagany jest kod resetowania hasła. Skontaktuj się z administratorem.", "passwordBack": "Powrót do hasła", "loginBack": "Wróć do logowania", "signup": "Zarejestruj się", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Lista zasobów strony", "actionUpdateSiteResource": "Aktualizuj zasób strony", "actionListInvitations": "Lista zaproszeń", + "actionExportLogs": "Eksportuj dzienniki", + "actionViewLogs": "Zobacz dzienniki", "noneSelected": "Nie wybrano", "orgNotFound2": "Nie znaleziono organizacji.", "searchProgress": "Szukaj...", "create": "Utwórz", "orgs": "Organizacje", "loginError": "Wystąpił błąd podczas logowania", + "loginRequiredForDevice": "Logowanie jest wymagane do uwierzytelnienia urządzenia.", "passwordForgot": "Zapomniałeś hasła?", "otpAuth": "Uwierzytelnianie dwuskładnikowe", "otpAuthDescription": "Wprowadź kod z aplikacji uwierzytelniającej lub jeden z jednorazowych kodów zapasowych.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Strona główna", "sidebarSites": "Witryny", "sidebarResources": "Zasoby", + "sidebarProxyResources": "Publiczne", + "sidebarClientResources": "Prywatny", "sidebarAccessControl": "Kontrola dostępu", + "sidebarLogsAndAnalytics": "Logi i Analityki", "sidebarUsers": "Użytkownicy", + "sidebarAdmin": "Administrator", "sidebarInvitations": "Zaproszenia", "sidebarRoles": "Role", - "sidebarShareableLinks": "Linki do udostępnienia", + "sidebarShareableLinks": "Linki", "sidebarApiKeys": "Klucze API", "sidebarSettings": "Ustawienia", "sidebarAllUsers": "Wszyscy użytkownicy", "sidebarIdentityProviders": "Dostawcy tożsamości", "sidebarLicense": "Licencja", "sidebarClients": "Klientami", + "sidebarUserDevices": "Użytkownicy", + "sidebarMachineClients": "Maszyny", "sidebarDomains": "Domeny", + "sidebarGeneral": "Ogólny", + "sidebarLogAndAnalytics": "Dziennik & Analityka", "sidebarBluePrints": "Schematy", + "sidebarOrganization": "Organizacja", + "sidebarLogsAnalytics": "Analityka", "blueprints": "Schematy", "blueprintsDescription": "Zastosuj konfiguracje deklaracyjne i wyświetl poprzednie operacje", "blueprintAdd": "Dodaj schemat", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Zobacz wynik zastosowanego schematu i wszelkie błędy, które wystąpiły", "blueprintInfo": "Informacje o projekcie", "message": "Wiadomość", - "blueprintContentsDescription": "Zdefiniuj zawartość YAML opisującą Twoją infrastrukturę", + "blueprintContentsDescription": "Zdefiniuj zawartość YAML opisującą infrastrukturę", "blueprintErrorCreateDescription": "Wystąpił błąd podczas stosowania schematu", "blueprintErrorCreate": "Błąd podczas tworzenia schematu", "searchBlueprintProgress": "Szukaj schematów...", @@ -1230,15 +1265,15 @@ "loading": "Ładowanie", "restart": "Uruchom ponownie", "domains": "Domeny", - "domainsDescription": "Zarządzaj domenami swojej organizacji", + "domainsDescription": "Tworzenie domen dostępnych w organizacji i zarządzanie nimi", "domainsSearch": "Szukaj domen...", "domainAdd": "Dodaj domenę", - "domainAddDescription": "Zarejestruj nową domenę w swojej organizacji", + "domainAddDescription": "Zarejestruj nową domenę w organizacji", "domainCreate": "Utwórz domenę", "domainCreatedDescription": "Domena utworzona pomyślnie", "domainDeletedDescription": "Domena usunięta pomyślnie", - "domainQuestionRemove": "Czy na pewno chcesz usunąć domenę ze swojego konta?", - "domainMessageRemove": "Po usunięciu domena nie będzie już powiązana z twoim kontem.", + "domainQuestionRemove": "Czy na pewno chcesz usunąć domenę?", + "domainMessageRemove": "Po usunięciu, domena nie będzie już powiązana z organizacją.", "domainConfirmDelete": "Potwierdź usunięcie domeny", "domainDelete": "Usuń domenę", "domain": "Domena", @@ -1257,7 +1292,7 @@ "pending": "Oczekuje", "sidebarBilling": "Fakturowanie", "billing": "Fakturowanie", - "orgBillingDescription": "Zarządzaj swoimi informacjami rozliczeniowymi i subskrypcjami", + "orgBillingDescription": "Zarządzaj informacjami rozliczeniowymi i subskrypcjami", "github": "GitHub", "pangolinHosted": "Logo Pangolin", "fossorial": "Fossorial", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Aktualizacje produktu", "productUpdateEmpty": "Brak aktualizacji", "dismissAll": "Zamknij wszystkie", - "pangolinUpdateAvailable": "Dostępna jest nowa wersja", + "pangolinUpdateAvailable": "Dostępna aktualizacja", "pangolinUpdateAvailableInfo": "Wersja {version} jest gotowa do zainstalowania", - "pangolinUpdateAvailableReleaseNotes": "Zobacz notatki o wydaniu", + "pangolinUpdateAvailableReleaseNotes": "Zobacz informacje o wydaniu", "newtUpdateAvailable": "Dostępna aktualizacja", "newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.", "domainPickerEnterDomain": "Domena", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Sprawdzanie dostępności...", - "domainPickerNoMatchingDomains": "Nie znaleziono pasujących domen. Spróbuj innej domeny lub sprawdź ustawienia domeny swojej organizacji.", + "domainPickerNoMatchingDomains": "Nie znaleziono pasujących domen. Wypróbuj inną domenę lub sprawdź ustawienia domeny organizacji.", "domainPickerOrganizationDomains": "Domeny organizacji", "domainPickerProvidedDomains": "Dostarczone domeny", "domainPickerSubdomain": "Subdomena: {subdomain}", @@ -1345,9 +1380,9 @@ "billingPortalError": "Błąd Portalu", "billingDataUsageInfo": "Jesteś obciążony za wszystkie dane przesyłane przez bezpieczne tunele, gdy jesteś podłączony do chmury. Obejmuje to zarówno ruch przychodzący, jak i wychodzący we wszystkich Twoich witrynach. Gdy osiągniesz swój limit, twoje strony zostaną rozłączone, dopóki nie zaktualizujesz planu lub nie ograniczysz użycia. Dane nie będą naliczane przy użyciu węzłów.", "billingOnlineTimeInfo": "Opłata zależy od tego, jak długo twoje strony pozostają połączone z chmurą. Na przykład 44,640 minut oznacza jedną stronę działającą 24/7 przez cały miesiąc. Kiedy osiągniesz swój limit, twoje strony zostaną rozłączone, dopóki nie zaktualizujesz planu lub nie zmniejsz jego wykorzystania. Czas nie będzie naliczany przy użyciu węzłów.", - "billingUsersInfo": "Jesteś obciążany za każdego użytkownika w twojej organizacji. Rozliczenia są obliczane codziennie na podstawie liczby aktywnych kont użytkowników w twojej organizacji.", - "billingDomainInfo": "Jesteś obciążany za każdą domenę w twojej organizacji. Rozliczenia są obliczane codziennie na podstawie liczby aktywnych kont domen w twojej organizacji.", - "billingRemoteExitNodesInfo": "Jesteś obciążany za każdy zarządzany węzeł w twojej organizacji. Rozliczenia są obliczane codziennie na podstawie liczby aktywnych zarządzanych węzłów w twojej organizacji.", + "billingUsersInfo": "Opłata za każdego użytkownika w organizacji. Płatność jest obliczana codziennie na podstawie liczby aktywnych kont użytkowników w Twojej organizacji.", + "billingDomainInfo": "Opłata za każdą domenę w organizacji. Płatność jest obliczana codziennie na podstawie liczby aktywnych kont domen w Twojej organizacji.", + "billingRemoteExitNodesInfo": "Opłata za każdy zarządzany węzeł w organizacji. Płatność jest obliczana codziennie na podstawie liczby aktywnych zarządzanych węzłów w Twojej organizacji.", "domainNotFound": "Nie znaleziono domeny", "domainNotFoundDescription": "Zasób jest wyłączony, ponieważ domena nie istnieje już w naszym systemie. Proszę ustawić nową domenę dla tego zasobu.", "failed": "Niepowodzenie", @@ -1430,29 +1465,32 @@ "and": "oraz", "privacyPolicy": "polityką prywatności" }, + "signUpMarketing": { + "keepMeInTheLoop": "Zachowaj mnie w pętli z wiadomościami, aktualizacjami i nowymi funkcjami przez e-mail." + }, "siteRequired": "Strona jest wymagana.", "olmTunnel": "Tunel Olm", "olmTunnelDescription": "Użyj Olm do łączności klienta", "errorCreatingClient": "Błąd podczas tworzenia klienta", "clientDefaultsNotFound": "Nie znaleziono domyślnych ustawień klienta", "createClient": "Utwórz Klienta", - "createClientDescription": "Utwórz nowego klienta do łączenia się z Twoimi witrynami", + "createClientDescription": "Utwórz nowego klienta, aby uzyskać dostęp do prywatnych zasobów", "seeAllClients": "Zobacz Wszystkich Klientów", "clientInformation": "Informacje o Kliencie", "clientNamePlaceholder": "Nazwa klienta", "address": "Adres", "subnetPlaceholder": "Podsieć", - "addressDescription": "Adres, którego ten klient będzie używać do łączności", + "addressDescription": "Adres wewnętrzny klienta. Musi mieścić się w podsieci organizacji.", "selectSites": "Wybierz witryny", "sitesDescription": "Klient będzie miał łączność z wybranymi witrynami", "clientInstallOlm": "Zainstaluj Olm", "clientInstallOlmDescription": "Uruchom Olm na swoim systemie", - "clientOlmCredentials": "Poświadczenia Olm", - "clientOlmCredentialsDescription": "To jest sposób, w jaki Olm będzie się uwierzytelniać z serwerem", - "olmEndpoint": "Punkt Końcowy Olm", - "olmId": "Identyfikator Olm", - "olmSecretKey": "Tajny Klucz Olm", - "clientCredentialsSave": "Zapisz swoje poświadczenia", + "clientOlmCredentials": "Dane logowania", + "clientOlmCredentialsDescription": "W ten sposób klient będzie uwierzytelniał się z serwerem", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Sekret", + "clientCredentialsSave": "Zapisz dane logowania", "clientCredentialsSaveDescription": "Będziesz mógł zobaczyć to tylko raz. Upewnij się, że skopiujesz go w bezpieczne miejsce.", "generalSettingsDescription": "Skonfiguruj ogólne ustawienia dla tego klienta", "clientUpdated": "Klient zaktualizowany", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Wystąpił błąd podczas pobierania witryn.", "olmErrorFetchReleases": "Wystąpił błąd podczas pobierania wydań Olm.", "olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.", - "remoteSubnets": "Zdalne Podsieci", "enterCidrRange": "Wprowadź zakres CIDR", - "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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Monitoruj zdrowie tego celu. Możesz monitorować inny punkt końcowy niż docelowy w razie potrzeby.", "healthScheme": "Metoda", "healthSelectScheme": "Wybierz metodę", + "healthCheckPortInvalid": "Port oceny stanu musi znajdować się między 1 a 65535", "healthCheckPath": "Ścieżka", "healthHostname": "IP / Nazwa hosta", "healthPort": "Port", "healthCheckPathDescription": "Ścieżka do sprawdzania stanu zdrowia.", - "healthyIntervalSeconds": "Interwał Zdrowy", - "unhealthyIntervalSeconds": "Interwał Niezdrowy", + "healthyIntervalSeconds": "Odstęp zdrowego (sek)", + "unhealthyIntervalSeconds": "Niezdrowy interwał (sek)", "IntervalSeconds": "Interwał Zdrowy", - "timeoutSeconds": "Limit Czasu", + "timeoutSeconds": "Limit czasu (sek)", "timeIsInSeconds": "Czas w sekundach", "retryAttempts": "Próby Ponowienia", "expectedResponseCodes": "Oczekiwane Kody Odpowiedzi", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Edytuj domenę", "siteName": "Nazwa strony", "proxyPort": "Port", - "resourcesTableProxyResources": "Zasoby proxy", - "resourcesTableClientResources": "Zasoby klienta", + "resourcesTableProxyResources": "Publiczne", + "resourcesTableClientResources": "Prywatny", "resourcesTableNoProxyResourcesFound": "Nie znaleziono zasobów proxy.", "resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.", "resourcesTableDestination": "Miejsce docelowe", - "resourcesTableTheseResourcesForUseWith": "Te zasoby są do użytku z", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Klientami", "resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.", "resourcesTableNoTargets": "Brak celów", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Offline", "resourcesTableUnknown": "Nieznane", "resourcesTableNotMonitored": "Nie monitorowano", - "editInternalResourceDialogEditClientResource": "Edytuj zasób klienta", - "editInternalResourceDialogUpdateResourceProperties": "Zaktualizuj właściwości zasobu i konfigurację celu dla {resourceName}.", + "editInternalResourceDialogEditClientResource": "Edytuj Zasoby Prywatne", + "editInternalResourceDialogUpdateResourceProperties": "Aktualizuj konfigurację zasobów i kontrolę dostępu dla {resourceName}", "editInternalResourceDialogResourceProperties": "Właściwości zasobów", "editInternalResourceDialogName": "Nazwa", "editInternalResourceDialogProtocol": "Protokół", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP", "editInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1", "editInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536", + "editInternalResourceDialogPortModeRequired": "Protokół, port proxy i port docelowy są wymagane dla trybu portu", + "editInternalResourceDialogMode": "Tryb", + "editInternalResourceDialogModePort": "Port", + "editInternalResourceDialogModeHost": "Host", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Miejsce docelowe", + "editInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.", + "editInternalResourceDialogDestinationIPDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.", + "editInternalResourceDialogDestinationCidrDescription": "Zakres CIDR zasobu w sieci witryny.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.", "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ą.", + "createInternalResourceDialogCreateClientResource": "Utwórz zasób prywatny", + "createInternalResourceDialogCreateClientResourceDescription": "Utwórz nowy zasób, który będzie dostępny tylko dla klientów podłączonych do organizacji", "createInternalResourceDialogResourceProperties": "Właściwości zasobów", "createInternalResourceDialogName": "Nazwa", "createInternalResourceDialogSite": "Witryna", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP", "createInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1", "createInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536", + "createInternalResourceDialogPortModeRequired": "Protokół, port proxy i port docelowy są wymagane dla trybu portu", + "createInternalResourceDialogMode": "Tryb", + "createInternalResourceDialogModePort": "Port", + "createInternalResourceDialogModeHost": "Host", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Miejsce docelowe", + "createInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.", + "createInternalResourceDialogDestinationCidrDescription": "Zakres CIDR zasobu w sieci witryny.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.", "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.", + "siteAcceptClientConnectionsDescription": "Zezwalaj urządzeniom i klientom na dostęp do zasobów na tej stronie. Może to zostać zmienione później.", + "siteAddress": "Adres witryny (Zaawansowany)", + "siteAddressDescription": "Adres wewnętrzny witryny. Musi mieścić się w podsieci organizacji.", + "siteNameDescription": "Wyświetlana nazwa witryny, która może zostać zmieniona później.", "autoLoginExternalIdp": "Automatyczny login z zewnętrznym IDP", "autoLoginExternalIdpDescription": "Natychmiastowe przekierowanie użytkownika do zewnętrznego IDP w celu uwierzytelnienia.", "selectIdp": "Wybierz IDP", @@ -1623,7 +1682,7 @@ "sidebarRemoteExitNodes": "Zdalne węzły", "remoteExitNodeCreate": { "title": "Utwórz węzeł", - "description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią", + "description": "Utwórz nowy węzeł, aby rozszerzyć łączność z siecią", "viewAllButton": "Zobacz wszystkie węzły", "strategy": { "title": "Strategia Tworzenia", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Typ dostawcy tożsamości", "roleMappingExpressionPlaceholder": "np. zawiera(grupy, 'admin') && 'Admin' || 'Członek'", "idpGoogleConfiguration": "Konfiguracja Google", - "idpGoogleConfigurationDescription": "Skonfiguruj swoje poświadczenia Google OAuth2", - "idpGoogleClientIdDescription": "Twój identyfikator klienta Google OAuth2", - "idpGoogleClientSecretDescription": "Twój klucz klienta Google OAuth2", + "idpGoogleConfigurationDescription": "Skonfiguruj dane logowania Google OAuth2", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Klucz tajny klienta Google OAuth2", "idpAzureConfiguration": "Konfiguracja Azure Entra ID", - "idpAzureConfigurationDescription": "Skonfiguruj swoje dane logowania OAuth2 Azure Entra", + "idpAzureConfigurationDescription": "Skonfiguruj poświadczenia Aure Entra ID OAuth2", "idpTenantId": "ID Najemcy", - "idpTenantIdPlaceholder": "twoj-lokator", - "idpAzureTenantIdDescription": "Twój identyfikator dzierżawcy Azure (znaleziony w Podglądzie Azure Active Directory", - "idpAzureClientIdDescription": "Twój identyfikator klienta rejestracji aplikacji Azure", - "idpAzureClientSecretDescription": "Klucz tajny Twojego klienta rejestracji aplikacji Azure", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "Identyfikator dzierżawcy azure (znaleziony w Azuure Active Directory podglądu)", + "idpAzureClientIdDescription": "Identyfikator klienta aplikacji Azure", + "idpAzureClientSecretDescription": "Klucz tajny klienta aplikacji Azure", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Konfiguracja Google", "idpAzureConfigurationTitle": "Konfiguracja Azure Entra ID", "idpTenantIdLabel": "ID Najemcy", - "idpAzureClientIdDescription2": "Twój identyfikator klienta rejestracji aplikacji Azure", - "idpAzureClientSecretDescription2": "Klucz tajny Twojego klienta rejestracji aplikacji Azure", + "idpAzureClientIdDescription2": "Identyfikator klienta aplikacji Azure", + "idpAzureClientSecretDescription2": "Klucz tajny klienta aplikacji Azure", "idpGoogleDescription": "Dostawca Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Podsieć", "subnetDescription": "Podsieć dla konfiguracji sieci tej organizacji.", "authPage": "Strona uwierzytelniania", - "authPageDescription": "Skonfiguruj stronę uwierzytelniania dla swojej organizacji", + "authPageDescription": "Skonfiguruj stronę uwierzytelniania dla organizacji", "authPageDomain": "Domena strony uwierzytelniania", "noDomainSet": "Nie ustawiono domeny", "changeDomain": "Zmień domenę", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Ustaw domenę strony uwierzytelniania", "failedToFetchCertificate": "Nie udało się pobrać certyfikatu", "failedToRestartCertificate": "Nie udało się ponownie uruchomić certyfikatu", - "addDomainToEnableCustomAuthPages": "Dodaj domenę, aby włączyć niestandardowe strony uwierzytelniania dla Twojej organizacji", + "addDomainToEnableCustomAuthPages": "Dodaj domenę, aby włączyć niestandardowe strony uwierzytelniania dla organizacji", "selectDomainForOrgAuthPage": "Wybierz domenę dla strony uwierzytelniania organizacji", "domainPickerProvidedDomain": "Dostarczona domena", "domainPickerFreeProvidedDomain": "Darmowa oferowana domena", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" nie może być poprawne dla {domain}.", "domainPickerSubdomainSanitized": "Poddomena oczyszczona", "domainPickerSubdomainCorrected": "\"{sub}\" został skorygowany do \"{sanitized}\"", - "orgAuthSignInTitle": "Zaloguj się do swojej organizacji", + "orgAuthSignInTitle": "Zaloguj się do organizacji", "orgAuthChooseIdpDescription": "Wybierz swojego dostawcę tożsamości, aby kontynuować", "orgAuthNoIdpConfigured": "Ta organizacja nie ma skonfigurowanych żadnych dostawców tożsamości. Zamiast tego możesz zalogować się za pomocą swojej tożsamości Pangolin.", "orgAuthSignInWithPangolin": "Zaloguj się używając Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Włącz uwierzytelnianie dwuskładnikowe", "completeSecuritySteps": "Zakończ kroki bezpieczeństwa", "securitySettings": "Ustawienia zabezpieczeń", - "securitySettingsDescription": "Skonfiguruj politykę bezpieczeństwa dla Twojej organizacji", + "securitySettingsDescription": "Skonfiguruj polityki bezpieczeństwa dla organizacji", "requireTwoFactorForAllUsers": "Wymagaj uwierzytelniania dwuetapowego dla wszystkich użytkowników", "requireTwoFactorDescription": "Po włączeniu wszyscy użytkownicy wewnętrzni w tej organizacji muszą mieć włączone uwierzytelnianie dwuskładnikowe, aby uzyskać dostęp do organizacji.", "requireTwoFactorDisabledDescription": "Ta funkcja wymaga poprawnej licencji (Enterprise) lub aktywnej subskrypcji (SaaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Edycja Enterprise", "unlicensed": "Nielicencjonowane", "beta": "Beta", - "manageClients": "Zarządzaj klientami", - "manageClientsDescription": "Klienci to urządzenia, które mogą łączyć się z Twoimi witrynami", + "manageUserDevices": "Urządzenia użytkownika", + "manageUserDevicesDescription": "Przeglądaj i zarządzaj urządzeniami, które użytkownicy używają do prywatnego łączenia się z zasobami", + "manageMachineClients": "Zarządzaj klientami maszyn", + "manageMachineClientsDescription": "Tworzenie i zarządzanie klientami, których serwery i systemy używają do prywatnego łączenia się z zasobami", + "clientsTableUserClients": "Użytkownik", + "clientsTableMachineClients": "Maszyna", "licenseTableValidUntil": "Ważny do", "saasLicenseKeysSettingsTitle": "Licencje przedsiębiorstwa", "saasLicenseKeysSettingsDescription": "Generuj i zarządzaj kluczami licencyjnymi Enterprise dla samodzielnych instancji Pangolin", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "pasek", "sidebarEnableEnterpriseLicense": "Włącz licencję przedsiębiorstwa", "cannotbeUndone": "Tej operacji nie można cofnąć.", - "toConfirm": "potwierdzić", + "toConfirm": "do potwierdzenia.", "deleteClientQuestion": "Czy na pewno chcesz usunąć klienta z witryny i organizacji?", "clientMessageRemove": "Po usunięciu, klient nie będzie już mógł połączyć się z witryną.", "sidebarLogs": "Logi", "request": "Żądanie", + "requests": "Żądania", "logs": "Logi", "logsSettingsDescription": "Monitoruj logi zebrane z tej orginizacji", "searchLogs": "Szukaj dzienników...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Powód", "requestLogs": "Dzienniki żądań", + "requestAnalytics": "Żądanie Analityki", "host": "Host", "location": "Lokalizacja", "actionLogs": "Dzienniki działań", @@ -2029,6 +2094,7 @@ "logRetention": "Zachowanie dziennika", "logRetentionDescription": "Zarządzaj jak długo różne typy logów są zachowane dla tej organizacji lub wyłącz je", "requestLogsDescription": "Zobacz szczegółowe dzienniki żądań zasobów w tej organizacji", + "requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji", "logRetentionRequestLabel": "Zachowanie dziennika żądań", "logRetentionRequestDescription": "Jak długo zachować dzienniki żądań", "logRetentionAccessLabel": "Zachowanie dziennika dostępu", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 dni", "logRetention90Days": "90 dni", "logRetentionForever": "Na zawsze", + "logRetentionEndOfFollowingYear": "Koniec następnego roku", "actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji", "accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji", "licenseRequiredToUse": "Licencja Enterprise jest wymagana do korzystania z tej funkcji.", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Dostępna jest zaktualizowana wersja Olm. Zaktualizuj do najnowszej wersji, aby uzyskać najlepsze doświadczenia.", "client": "Klient", "proxyProtocol": "Ustawienia protokołu proxy", - "proxyProtocolDescription": "Skonfiguruj protokół Proxy aby zachować adresy IP klienta dla usług TCP/UDP.", + "proxyProtocolDescription": "Skonfiguruj protokół Proxy aby zachować adresy IP klienta dla usług TCP.", "enableProxyProtocol": "Włącz protokół proxy", - "proxyProtocolInfo": "Zachowaj adresy IP klienta dla backendów TCP/UDP", + "proxyProtocolInfo": "Zachowaj adresy IP klienta dla backendów TCP", "proxyProtocolVersion": "Wersja protokołu proxy", "version1": " Wersja 1 (zalecane)", "version2": "Wersja 2", "versionDescription": "Wersja 1 jest oparta na tekście i szeroko wspierana. Wersja 2 jest binarna i bardziej efektywna, ale mniej kompatybilna.", "warning": "Ostrzeżenie", - "proxyProtocolWarning": "Twoja aplikacja backend musi być skonfigurowana tak, aby przyjmować połączenia z protokołem proxy. Jeśli Twój backend nie obsługuje protokołu proxy, włączenie to spowoduje przerwanie wszystkich połączeń. Upewnij się, że konfiguracja twojego backendu do zaufanych nagłówków protokołu proxy z Traefik.", + "proxyProtocolWarning": "Aplikacja backend musi być skonfigurowana do akceptowania połączeń protokołu proxy. Jeśli Twój backend nie obsługuje protokołu Proxy, włączenie tego spowoduje przerwanie wszystkich połączeń, więc włącz to tylko jeśli wiesz, co robisz. Upewnij się, że konfiguracja twojego backendu do zaufanych nagłówków protokołu proxy z Traefik.", "restarting": "Restartowanie...", "manual": "Ręcznie", "messageSupport": "Obsługa wiadomości", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Wiadomość wysłana!", "supportWillContact": "Wkrótce będziemy w kontakcie!", "selectLogRetention": "Wybierz zatrzymanie dziennika", + "terms": "Regulamin", + "privacy": "Prywatność", + "security": "Bezpieczeństwo", + "docs": "Dokumentacja", + "deviceActivation": "Aktywacja urządzenia", + "deviceCodeInvalidFormat": "Kod musi mieć 9 znaków (np. A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Nieprawidłowy lub wygasły kod", + "deviceCodeVerifyFailed": "Nie udało się zweryfikować kodu urządzenia", + "signedInAs": "Zalogowany jako", + "deviceCodeEnterPrompt": "Wprowadź kod wyświetlany na urządzeniu", + "continue": "Kontynuuj", + "deviceUnknownLocation": "Nieznana lokalizacja", + "deviceAuthorizationRequested": "Ta autoryzacja została zgłoszona do {location} na {date}. Upewnij się, że ufasz urządzeniu, ponieważ uzyska dostęp do konta.", + "deviceLabel": "Urządzenie: {deviceName}", + "deviceWantsAccess": "chce uzyskać dostęp do Twojego konta", + "deviceExistingAccess": "Istniejący dostęp:", + "deviceFullAccess": "Pełny dostęp do Twojego konta", + "deviceOrganizationsAccess": "Dostęp do wszystkich organizacji, do których Twoje konto ma dostęp", + "deviceAuthorize": "Autoryzuj {applicationName}", + "deviceConnected": "Urządzenie podłączone!", + "deviceAuthorizedMessage": "Urządzenie jest upoważnione do dostępu do Twojego konta.", + "pangolinCloud": "Chmura Pangolin", + "viewDevices": "Zobacz urządzenia", + "viewDevicesDescription": "Zarządzaj podłączonymi urządzeniami", + "noDevices": "Nie znaleziono urządzeń", + "dateCreated": "Data utworzenia", + "unnamedDevice": "Urządzenie bez nazwy", + "deviceQuestionRemove": "Czy na pewno chcesz usunąć to urządzenie?", + "deviceMessageRemove": "Tej czynności nie można cofnąć.", + "deviceDeleteConfirm": "Usuń urządzenie", + "deleteDevice": "Usuń urządzenie", + "errorLoadingDevices": "Błąd ładowania urządzeń", + "failedToLoadDevices": "Nie udało się załadować urządzeń", + "deviceDeleted": "Urządzenie usunięte", + "deviceDeletedDescription": "Urządzenie zostało usunięte.", + "errorDeletingDevice": "Błąd podczas usuwania urządzenia", + "failedToDeleteDevice": "Nie udało się usunąć urządzenia", "showColumns": "Pokaż kolumny", "hideColumns": "Ukryj kolumny", "columnVisibility": "Widoczność kolumn", @@ -2111,10 +2215,14 @@ "enableSelected": "Włącz zaznaczone", "disableSelected": "Wyłącz zaznaczone", "checkSelectedStatus": "Sprawdź status zaznaczonych", + "clients": "Klientami", + "accessClientSelect": "Wybierz klientów komputera", + "resourceClientDescription": "Klienci maszynowi, którzy mają dostęp do tego zasobu", + "regenerate": "Wygeneruj ponownie", "credentials": "Dane logowania", "savecredentials": "Zapisz dane logowania", - "regeneratecredentials": "Przycisk ponownie", - "regenerateCredentials": "Ponownie wygeneruj i zapisz swoje dane logowania", + "regenerateCredentialsButton": "Wygeneruj dane logowania", + "regenerateCredentials": "Wygeneruj dane logowania", "generatedcredentials": "Wygenerowane dane logowania", "copyandsavethesecredentials": "Skopiuj i zapisz te dane logowania", "copyandsavethesecredentialsdescription": "Te dane uwierzytelniające nie będą wyświetlane ponownie po opuszczeniu tej strony. Zapisz je teraz bezpiecznie.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Dane logowania zostały wygenerowane i zapisane pomyślnie.", "credentialsSaveError": "Błąd zapisu danych logowania", "credentialsSaveErrorDescription": "Wystąpił błąd podczas regeneracji i zapisywania poświadczeń.", - "regenerateCredentialsWarning": "Regeneracja poświadczeń spowoduje unieważnienie poprzednich poświadczeń. Upewnij się, że zaktualizowano wszystkie konfiguracje, które używają tych poświadczeń.", + "regenerateCredentialsWarning": "Regeneracja poświadczeń spowoduje unieważnienie poprzednich danych i spowoduje rozłączenie. Upewnij się, że aktualizacja wszystkich konfiguracji, które używają tych poświadczeń.", "confirm": "Potwierdź", "regenerateCredentialsConfirmation": "Czy na pewno chcesz wygenerować dane logowania?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Sekretny klucz", - "featureDisabledTooltip": "Ta funkcja jest dostępna tylko w planie przedsiębiorstwa i wymaga licencji, aby z niej korzystać.", "niceId": "Niepoprawne ID", "niceIdUpdated": "Zaktualizowano błędne ID", "niceIdUpdatedSuccessfully": "Zaktualizowano błędne ID", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Wystąpił błąd podczas aktualizowania Nicei ID.", "niceIdCannotBeEmpty": "Niepoprawny identyfikator nie może być pusty", "enterIdentifier": "Wprowadź identyfikator", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "To nie? Użyj innego konta.", + "deviceLoginDeviceRequestingAccessToAccount": "Urządzenie żąda dostępu do tego konta.", + "noData": "Brak danych", + "machineClients": "Klienci maszyn", + "install": "Zainstaluj", + "run": "Uruchom", + "clientNameDescription": "Wyświetlana nazwa klienta, która może zostać zmieniona później.", + "clientAddress": "Adres klienta (Zaawansowany)", + "setupFailedToFetchSubnet": "Nie udało się pobrać domyślnej podsieci", + "setupSubnetAdvanced": "Podsieć (zaawansowana)", + "setupSubnetDescription": "Podsieć dla wewnętrznej sieci tej organizacji.", + "siteRegenerateAndDisconnect": "Wygeneruj ponownie i rozłącz", + "siteRegenerateAndDisconnectConfirmation": "Czy na pewno chcesz odzyskać dane logowania i odłączyć tę stronę?", + "siteRegenerateAndDisconnectWarning": "Spowoduje to regenerację poświadczeń i natychmiastowe odłączenie witryny. Strona będzie musiała zostać zrestartowana z nowymi poświadczeniami.", + "siteRegenerateCredentialsConfirmation": "Czy na pewno chcesz odzyskać dane logowania dla tej witryny?", + "siteRegenerateCredentialsWarning": "Spowoduje to regenerację poświadczeń. Witryna pozostanie połączona dopóki nie uruchomisz jej ręcznie i nie użyjesz nowych poświadczeń.", + "clientRegenerateAndDisconnect": "Wygeneruj ponownie i rozłącz", + "clientRegenerateAndDisconnectConfirmation": "Czy na pewno chcesz odzyskać dane logowania i odłączyć tego klienta?", + "clientRegenerateAndDisconnectWarning": "Spowoduje to regenerację poświadczeń i natychmiastowe odłączenie klienta. Klient będzie musiał zostać ponownie uruchomiony z nowymi poświadczeniami.", + "clientRegenerateCredentialsConfirmation": "Czy na pewno chcesz odzyskać dane logowania dla tego klienta?", + "clientRegenerateCredentialsWarning": "Spowoduje to regenerację poświadczeń. Klient pozostanie połączony dopóki nie uruchomisz go ponownie i nie użyjesz nowych poświadczeń.", + "remoteExitNodeRegenerateAndDisconnect": "Wygeneruj ponownie i rozłącz", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Czy na pewno chcesz odzyskać dane logowania i odłączyć ten węzeł zdalnego wyjścia?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Spowoduje to regenerację danych logowania i natychmiastowe odłączenie zdalnego węzła wyjścia. Węzeł zdalnego wyjścia będzie musiał zostać ponownie uruchomiony z nowymi danymi logowania.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Czy na pewno chcesz wygenerować dane logowania dla tego węzła zdalnego wyjścia?", + "remoteExitNodeRegenerateCredentialsWarning": "Spowoduje to regenerację poświadczeń. Serwer wyjścia pozostanie podłączony do momentu ręcznego ponownego uruchomienia i użycia nowych poświadczeń.", + "agent": "Agent" } diff --git a/messages/pt-PT.json b/messages/pt-PT.json index ed9a4551..41ae04b0 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -1,12 +1,12 @@ { - "setupCreate": "Crie sua organização, site e recursos", + "setupCreate": "Criar a organização, o site e os recursos", "setupNewOrg": "Nova organização", "setupCreateOrg": "Criar Organização", "setupCreateResources": "Criar recursos", "setupOrgName": "Nome Da Organização", - "orgDisplayName": "Este é o nome de exibição da sua organização.", + "orgDisplayName": "Este é o nome de exibição da organização.", "orgId": "ID da organização", - "setupIdentifierMessage": "Este é o identificador exclusivo para sua organização. Isso é separado do nome de exibição.", + "setupIdentifierMessage": "Este é o identificador exclusivo da organização.", "setupErrorIdentifier": "O ID da organização já existe. Por favor, escolha um diferente.", "componentsErrorNoMemberCreate": "Não é atualmente um membro de nenhuma organização. Crie uma organização para começar.", "componentsErrorNoMember": "Não é atualmente um membro de nenhuma organização.", @@ -50,10 +50,10 @@ "siteMessageRemove": "Uma vez removido, o site não estará mais acessível. Todas as metas associadas ao site também serão removidas.", "siteQuestionRemove": "Você tem certeza que deseja remover este site da organização?", "siteManageSites": "Gerir sites", - "siteDescription": "Permitir conectividade à sua rede através de túneis seguros", + "siteDescription": "Criar e gerenciar sites para ativar a conectividade a redes privadas", "siteCreate": "Criar site", "siteCreateDescription2": "Siga os passos abaixo para criar e conectar um novo site", - "siteCreateDescription": "Crie um novo site para começar a conectar seus recursos", + "siteCreateDescription": "Crie um novo site para começar a conectar os recursos", "close": "FECHAR", "siteErrorCreate": "Erro ao criar site", "siteErrorCreateKeyPair": "Par de chaves ou padrões do site não encontrados", @@ -74,7 +74,7 @@ "siteInstallNewt": "Instalar Novo", "siteInstallNewtDescription": "Novo item em execução no seu sistema", "WgConfiguration": "Configuração do WireGuard", - "WgConfigurationDescription": "Use a seguinte configuração para conectar-se à sua rede", + "WgConfigurationDescription": "Use a seguinte configuração para conectar-se à rede", "operatingSystem": "Sistema operacional", "commands": "Comandos", "recommended": "Recomendados", @@ -87,32 +87,32 @@ "siteUpdated": "Site atualizado", "siteUpdatedDescription": "O site foi atualizado.", "siteGeneralDescription": "Configurar as configurações gerais para este site", - "siteSettingDescription": "Configure as configurações no seu site", + "siteSettingDescription": "Configurar as configurações no site", "siteSetting": "Configurações do {siteName}", - "siteNewtTunnel": "Novo túnel (recomendado)", - "siteNewtTunnelDescription": "A maneira mais fácil de criar um ponto de entrada na sua rede. Nenhuma configuração extra.", + "siteNewtTunnel": "Novo Site (Recomendado)", + "siteNewtTunnelDescription": "Maneira mais fácil de criar um ponto de entrada em qualquer 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 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": "Apenas recursos locais. Sem túneis. Apenas disponível em nós remotos.", "siteSeeAll": "Ver todos os sites", - "siteTunnelDescription": "Determine como você deseja se conectar ao seu site", - "siteNewtCredentials": "Credenciais Novas", - "siteNewtCredentialsDescription": "É assim que o novo sistema se autenticará com o servidor", - "siteCredentialsSave": "Salve suas credenciais", + "siteTunnelDescription": "Determine como você deseja se conectar ao site", + "siteNewtCredentials": "Credenciais", + "siteNewtCredentialsDescription": "É assim que o site se autentica com o servidor", + "siteCredentialsSave": "Salvar as Credenciais", "siteCredentialsSaveDescription": "Você só será capaz de ver esta vez. Certifique-se de copiá-lo para um lugar seguro.", "siteInfo": "Informações do Site", "status": "SItuação", "shareTitle": "Gerir links partilhados", - "shareDescription": "Criar links compartilháveis para conceder acesso temporário ou permanente aos seus recursos", + "shareDescription": "Criar links compartilháveis para conceder acesso temporário ou permanente aos recursos do proxy", "shareSearch": "Pesquisar links de compartilhamento...", "shareCreate": "Criar Link de Compartilhamento", "shareErrorDelete": "Falha ao apagar o link", "shareErrorDeleteMessage": "Ocorreu um erro ao apagar o link", "shareDeleted": "Link excluído", "shareDeletedDescription": "O link foi eliminado", - "shareTokenDescription": "Seu token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.", + "shareTokenDescription": "O token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.", "accessToken": "Token de acesso", "usageExamples": "Exemplos de uso", "tokenId": "ID do Token", @@ -121,7 +121,7 @@ "importantNote": "Nota importante", "shareImportantDescription": "Por razões de segurança, o uso de cabeçalhos é recomendado através dos parâmetros de consulta quando possível, já que os parâmetros de consulta podem estar logados nos logs do servidor ou no histórico do navegador.", "token": "Identificador", - "shareTokenSecurety": "Mantenha seu token de acesso seguro. Não o compartilhe em áreas de acesso público ou código do lado do cliente.", + "shareTokenSecurety": "Mantenha o token de acesso seguro. Não o compartilhe em áreas de acesso público ou código do lado do cliente.", "shareErrorFetchResource": "Falha ao buscar recursos", "shareErrorFetchResourceDescription": "Ocorreu um erro ao obter os recursos", "shareErrorCreate": "Falha ao criar link de compartilhamento", @@ -144,8 +144,10 @@ "expires": "Expira", "never": "nunca", "shareErrorSelectResource": "Por favor, selecione um recurso", - "resourceTitle": "Gerir Recursos", - "resourceDescription": "Crie proxies seguros para seus aplicativos privados", + "proxyResourceTitle": "Gerenciar Recursos Públicos", + "proxyResourceDescription": "Criar e gerenciar recursos que são acessíveis publicamente por meio de um navegador da web", + "clientResourceTitle": "Gerenciar recursos privados", + "clientResourceDescription": "Criar e gerenciar recursos que só são acessíveis por meio de um cliente conectado", "resourcesSearch": "Procurar recursos...", "resourceAdd": "Adicionar Recurso", "resourceErrorDelte": "Erro ao apagar recurso", @@ -155,9 +157,9 @@ "resourceMessageRemove": "Uma vez removido, o recurso não estará mais acessível. Todos os alvos associados ao recurso também serão removidos.", "resourceQuestionRemove": "Você tem certeza que deseja remover o recurso da organização?", "resourceHTTP": "Recurso HTTPS", - "resourceHTTPDescription": "O proxy solicita ao seu aplicativo via HTTPS usando um subdomínio ou domínio base.", + "resourceHTTPDescription": "O proxy solicita ao aplicativo via HTTPS usando um subdomínio ou domínio base.", "resourceRaw": "Recurso TCP/UDP bruto", - "resourceRawDescription": "O proxy solicita ao seu aplicativo sobre TCP/UDP usando um número de porta.", + "resourceRawDescription": "O proxy solicita ao aplicativo sobre TCP/UDP usando um número de porta. Isso só funciona quando os sites estão conectados a nós.", "resourceCreate": "Criar Recurso", "resourceCreateDescription": "Siga os passos abaixo para criar um novo recurso", "resourceSeeAll": "Ver todos os recursos", @@ -171,22 +173,22 @@ "noCountryFound": "Nenhum país encontrado.", "siteSelectionDescription": "Este site fornecerá conectividade ao destino.", "resourceType": "Tipo de Recurso", - "resourceTypeDescription": "Determine como você deseja aceder seu recurso", + "resourceTypeDescription": "Determine como acessar o recurso", "resourceHTTPSSettings": "Configurações de HTTPS", - "resourceHTTPSSettingsDescription": "Configure como seu recurso será acessado por HTTPS", + "resourceHTTPSSettingsDescription": "Configure como o recurso será acessado por HTTPS", "domainType": "Tipo de domínio", "subdomain": "Subdomínio", "baseDomain": "Domínio Base", - "subdomnainDescription": "O subdomínio onde seu recurso estará acessível.", + "subdomnainDescription": "O subdomínio onde o recurso será acessível.", "resourceRawSettings": "Configurações TCP/UDP", - "resourceRawSettingsDescription": "Configure como seu recurso será acessado sobre TCP/UDP. Você mapeia o recurso para uma porta no servidor Pangolin do hospedeiro, para que você possa acessar o recurso do server-public-ip:mapped-port.", + "resourceRawSettingsDescription": "Configurar como o recurso será acessado sobre TCP/UDP", "protocol": "Protocolo", "protocolSelect": "Selecione um protocolo", "resourcePortNumber": "Número da Porta", "resourcePortNumberDescription": "O número da porta externa para requisições de proxy.", "cancel": "cancelar", "resourceConfig": "Snippets de Configuração", - "resourceConfigDescription": "Copie e cole estes snippets de configuração para configurar o seu recurso TCP/UDP", + "resourceConfigDescription": "Copie e cole estes snippets de configuração para configurar o recurso TCP/UDP", "resourceAddEntrypoints": "Traefik: Adicionar pontos de entrada", "resourceExposePorts": "Gerbil: Expor Portas no Docker Compose", "resourceLearnRaw": "Aprenda como configurar os recursos TCP/UDP", @@ -202,14 +204,14 @@ "proxy": "Proxy", "internal": "Interno", "rules": "Regras", - "resourceSettingDescription": "Configure as configurações do seu recurso", + "resourceSettingDescription": "Configure as configurações do recurso", "resourceSetting": "Configurações do {resourceName}", - "alwaysAllow": "Sempre permitir", - "alwaysDeny": "Sempre negar", + "alwaysAllow": "Autenticação de bypass", + "alwaysDeny": "Bloquear Acesso", "passToAuth": "Passar para Autenticação", - "orgSettingsDescription": "Configurar as configurações gerais da sua organização", + "orgSettingsDescription": "Configurar configurações da organização", "orgGeneralSettings": "Configurações da organização", - "orgGeneralSettingsDescription": "Gerir os detalhes e a configuração da sua organização", + "orgGeneralSettingsDescription": "Gerenciar os detalhes e a configuração da organização", "saveGeneralSettings": "Guardar configurações gerais", "saveSettings": "Guardar Configurações", "orgDangerZone": "Zona de Perigo", @@ -232,7 +234,7 @@ "orgMissing": "ID da Organização Ausente", "orgMissingMessage": "Não é possível regenerar o convite sem um ID de organização.", "accessUsersManage": "Gerir Utilizadores", - "accessUsersDescription": "Convidar utilizadores e adicioná-los a funções para gerir o acesso à sua organização", + "accessUsersDescription": "Convidar e gerenciar usuários com acesso a esta organização", "accessUsersSearch": "Procurar utilizadores...", "accessUserCreate": "Criar Usuário", "accessUserRemove": "Remover utilizador", @@ -241,13 +243,13 @@ "role": "Funções", "nameRequired": "O nome é obrigatório", "accessRolesManage": "Gerir Funções", - "accessRolesDescription": "Configurar funções para gerir o acesso à sua organização", + "accessRolesDescription": "Criar e gerenciar funções para os usuários na organização", "accessRolesSearch": "Pesquisar funções...", "accessRolesAdd": "Adicionar função", "accessRoleDelete": "Excluir Papel", "description": "Descrição:", "inviteTitle": "Convites Abertos", - "inviteDescription": "Gerir seus convites para outros utilizadores", + "inviteDescription": "Gerenciar convites para outros usuários participarem da organização", "inviteSearch": "Procurar convites...", "minutes": "minutos", "hours": "horas", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Erro ao criar chave API", "apiKeysErrorSetPermission": "Erro ao definir permissões", "apiKeysCreate": "Gerar Chave API", - "apiKeysCreateDescription": "Gerar uma nova chave API para sua organização", + "apiKeysCreateDescription": "Gerar uma nova chave de API para a organização", "apiKeysGeneralSettings": "Permissões", "apiKeysGeneralSettingsDescription": "Determine o que esta chave API pode fazer", - "apiKeysList": "Sua Chave API", - "apiKeysSave": "Guardar Sua Chave API", + "apiKeysList": "Nova chave de API", + "apiKeysSave": "Salvar a chave API", "apiKeysSaveDescription": "Você só poderá ver isto uma vez. Certifique-se de copiá-la para um local seguro.", - "apiKeysInfo": "Sua chave API é:", + "apiKeysInfo": "A chave API é:", "apiKeysConfirmCopy": "Eu copiei a chave API", "generate": "Gerar", "done": "Concluído", @@ -424,7 +426,7 @@ "userCreated": "Usuário criado", "userCreatedDescription": "O utilizador foi criado com sucesso.", "userTypeInternal": "Usuário Interno", - "userTypeInternalDescription": "Convidar um utilizador para se juntar à sua organização diretamente.", + "userTypeInternalDescription": "Convide um usuário para participar diretamente da organização.", "userTypeExternal": "Usuário Externo", "userTypeExternalDescription": "Criar um utilizador com um provedor de identidade externo.", "accessUserCreateDescription": "Siga os passos abaixo para criar um novo utilizador", @@ -436,6 +438,16 @@ "inviteEmailSent": "Enviar e-mail de convite para o utilizador", "inviteValid": "Válido Por", "selectDuration": "Selecionar duração", + "selectResource": "Selecionar Recurso", + "filterByResource": "Filtrar por Recurso", + "resetFilters": "Redefinir filtros", + "totalBlocked": "Solicitações bloqueadas pelo Pangolin", + "totalRequests": "Total de pedidos", + "requestsByCountry": "Solicitações por país", + "requestsByDay": "Requisições Por Dia", + "blocked": "Bloqueado", + "allowed": "Permitido", + "topCountries": "Principais países", "accessRoleSelect": "Selecionar função", "inviteEmailSentDescription": "Um e-mail foi enviado ao utilizador com o link de acesso abaixo. Eles devem aceder ao link para aceitar o convite.", "inviteSentDescription": "O utilizador foi convidado. Eles devem aceder ao link abaixo para aceitar o convite.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Guardar Controlos de Acesso", "roles": "Funções", "accessUsersRoles": "Gerir Utilizadores e Funções", - "accessUsersRolesDescription": "Convide utilizadores e adicione-os a funções para gerir o acesso à sua organização", + "accessUsersRolesDescription": "Convidar usuários e adicioná-los a funções para gerenciar o acesso à organização", "key": "Chave", "createdAt": "Criado Em", "proxyErrorInvalidHeader": "Valor do cabeçalho Host personalizado inválido. Use o formato de nome de domínio ou salve vazio para remover o cabeçalho Host personalizado.", "proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.", "proxyEnableSSL": "Habilitar SSL", - "proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras a seus alvos.", + "proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras aos alvos.", "target": "Target", "configureTarget": "Configurar Alvos", "targetErrorFetch": "Falha ao buscar alvos", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Falha ao atualizar alvos", "targetsErrorUpdateDescription": "Ocorreu um erro ao atualizar alvos", "targetTlsUpdate": "Configurações TLS atualizadas", - "targetTlsUpdateDescription": "Suas configurações TLS foram atualizadas com sucesso", + "targetTlsUpdateDescription": "Configurações TLS foram atualizadas com sucesso", "targetErrorTlsUpdate": "Falha ao atualizar configurações TLS", "targetErrorTlsUpdateDescription": "Ocorreu um erro ao atualizar as configurações TLS", "proxyUpdated": "Configurações de proxy atualizadas", - "proxyUpdatedDescription": "Suas configurações de proxy foram atualizadas com sucesso", + "proxyUpdatedDescription": "Configurações de proxy atualizadas com sucesso", "proxyErrorUpdate": "Falha ao atualizar configurações de proxy", "proxyErrorUpdateDescription": "Ocorreu um erro ao atualizar as configurações de proxy", - "targetAddr": "IP / Nome do Host", + "targetAddr": "Servidor", "targetPort": "Porta", "targetProtocol": "Protocolo", "targetTlsSettings": "Configuração de conexão segura", - "targetTlsSettingsDescription": "Configurar configurações SSL/TLS para seu recurso", + "targetTlsSettingsDescription": "Configurar configurações SSL/TLS para o recurso", "targetTlsSettingsAdvanced": "Configurações TLS Avançadas", "targetTlsSni": "Nome do Servidor TLS", "targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.", "targetTlsSubmit": "Guardar Configurações", "targets": "Configuração de Alvos", - "targetsDescription": "Configure alvos para rotear tráfego para seus serviços de backend", + "targetsDescription": "Configurar alvos para tráfego de rota para 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", "targetSubmit": "Adicionar Alvo", - "targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para sua área de administração.", + "targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para o backend.", "targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.", "targetsSubmit": "Guardar Alvos", "addTarget": "Adicionar Alvo", @@ -516,9 +528,11 @@ "targetCreatedDescription": "O alvo foi criado com sucesso", "targetErrorCreate": "Falha ao criar destino", "targetErrorCreateDescription": "Ocorreu um erro ao criar o destino", + "tlsServerName": "Nome do Servidor TLS", + "tlsServerNameDescription": "O nome do servidor TLS a ser usado para SNI", "save": "Guardar", "proxyAdditional": "Configurações Adicionais de Proxy", - "proxyAdditionalDescription": "Configure como seu recurso lida com configurações de proxy", + "proxyAdditionalDescription": "Configurar como o recurso lida com as configurações de proxy", "proxyCustomHeader": "Cabeçalho Host Personalizado", "proxyCustomHeaderDescription": "O cabeçalho host para definir ao fazer proxy de requisições. Deixe vazio para usar o padrão.", "proxyAdditionalSubmit": "Guardar Configurações de Proxy", @@ -558,7 +572,7 @@ "rulesMatchType": "Tipo de Correspondência", "value": "Valor", "rulesAbout": "Sobre Regras", - "rulesAboutDescription": "As regras permitem controlar o acesso ao seu recurso com base em um conjunto de critérios. Você pode criar regras para permitir ou negar acesso com base no endereço IP ou caminho URL.", + "rulesAboutDescription": "Regras permitem que você controle o acesso ao recurso com base em um conjunto de critérios. Você pode criar regras para permitir ou negar acesso baseado no endereço IP ou caminho de URL.", "rulesActions": "Ações", "rulesActionAlwaysAllow": "Sempre Permitir: Ignorar todos os métodos de autenticação", "rulesActionAlwaysDeny": "Sempre Negar: Bloquear todas as requisições; nenhuma autenticação pode ser tentada", @@ -570,7 +584,7 @@ "rulesEnable": "Ativar Regras", "rulesEnableDescription": "Ativar ou desativar avaliação de regras para este recurso", "rulesResource": "Configuração de Regras do Recurso", - "rulesResourceDescription": "Configure regras para controlar o acesso ao seu recurso", + "rulesResourceDescription": "Configurar regras para controlar o acesso ao recurso", "ruleSubmit": "Adicionar Regra", "rulesNoOne": "Sem regras. Adicione uma regra usando o formulário.", "rulesOrder": "As regras são avaliadas por prioridade em ordem ascendente.", @@ -586,7 +600,7 @@ "none": "Nenhum", "unknown": "Desconhecido", "resources": "Recursos", - "resourcesDescription": "Recursos são proxies para aplicações executando em sua rede privada. Crie um recurso para qualquer serviço HTTP/HTTPS ou TCP/UDP bruto em sua rede privada. Cada recurso deve estar conectado a um site para habilitar conectividade privada e segura através de um túnel WireGuard criptografado.", + "resourcesDescription": "Os recursos são proxies para aplicativos em execução na rede privada. Crie um recurso para qualquer serviço HTTP/HTTPS ou TCP/UDP bruto na sua rede privada. Cada recurso deve ser conectado a um site para ativar a conectividade privada e segura através de um túnel do WireGuard criptografado.", "resourcesWireGuardConnect": "Conectividade segura com criptografia WireGuard", "resourcesMultipleAuthenticationMethods": "Configure múltiplos métodos de autenticação", "resourcesUsersRolesAccess": "Controle de acesso baseado em utilizadores e funções", @@ -597,7 +611,7 @@ "resourceSelect": "Selecionar recurso", "shareLinks": "Links de Compartilhamento", "share": "Links Compartilháveis", - "shareDescription2": "Crie links compartilháveis para seus recursos. Os links fornecem acesso temporário ou ilimitado ao seu recurso. Você pode configurar a duração da expiração do link quando o criar.", + "shareDescription2": "Crie links compartilháveis para recursos. Links fornecem acesso temporário ou ilimitado ao seu recurso. Você pode configurar a duração de expiração do link quando você criar um.", "shareEasyCreate": "Fácil de criar e compartilhar", "shareConfigurableExpirationDuration": "Duração de expiração configurável", "shareSecureAndRevocable": "Seguro e revogável", @@ -607,19 +621,19 @@ "unknownCommand": "Comando desconhecido", "newtErrorFetchReleases": "Falha ao buscar informações da versão: {err}", "newtErrorFetchLatest": "Erro ao buscar última versão: {err}", - "newtEndpoint": "Endpoint Newt", - "newtId": "ID Newt", - "newtSecretKey": "Chave Secreta Newt", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Chave Secreta", "architecture": "Arquitetura", "sites": "sites", - "siteWgAnyClients": "Use qualquer cliente WireGuard para conectar. Você terá que endereçar seus recursos internos usando o IP do par.", + "siteWgAnyClients": "Use qualquer cliente do WireGuard para se conectar. Você terá que endereçar recursos internos usando o IP de pares.", "siteWgCompatibleAllClients": "Compatível com todos os clientes WireGuard", "siteWgManualConfigurationRequired": "Configuração manual necessária", "userErrorNotAdminOrOwner": "Usuário não é administrador ou proprietário", "pangolinSettings": "Configurações - Pangolin", "accessRoleYour": "Sua função:", - "accessRoleSelect2": "Selecionar uma função", - "accessUserSelect": "Selecionar um utilizador", + "accessRoleSelect2": "Selecionar funções", + "accessUserSelect": "Selecionar os usuários", "otpEmailEnter": "Digite um e-mail", "otpEmailEnterDescription": "Pressione enter para adicionar um e-mail após digitá-lo no campo de entrada.", "otpEmailErrorInvalid": "Endereço de e-mail inválido. O caractere curinga (*) deve ser a parte local inteira.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Definir Código PIN", "resourcePincodeSetupTitleDescription": "Defina um código PIN para proteger este recurso", "resourceRoleDescription": "Administradores sempre podem aceder este recurso.", - "resourceUsersRoles": "Utilizadores e Funções", + "resourceUsersRoles": "Controlos de Acesso", "resourceUsersRolesDescription": "Configure quais utilizadores e funções podem visitar este recurso", "resourceUsersRolesSubmit": "Guardar Utilizadores e Funções", "resourceWhitelistSave": "Salvo com sucesso", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Transferir Recurso", "siteDestination": "Site de Destino", "searchSites": "Pesquisar sites", + "countries": "Países", "accessRoleCreate": "Criar Função", "accessRoleCreateDescription": "Crie uma nova função para agrupar utilizadores e gerir suas permissões.", "accessRoleCreateSubmit": "Criar Função", @@ -766,15 +781,15 @@ "idpOidcConfigure": "Configuração OAuth2/OIDC", "idpOidcConfigureDescription": "Configurar os endpoints e credenciais do provedor OAuth2/OIDC", "idpClientId": "ID do Cliente", - "idpClientIdDescription": "O ID do cliente OAuth2 do seu provedor de identidade", + "idpClientIdDescription": "O ID de cliente OAuth2 do provedor de identidade", "idpClientSecret": "Segredo do Cliente", - "idpClientSecretDescription": "O segredo do cliente OAuth2 do seu provedor de identidade", + "idpClientSecretDescription": "O segredo de cliente OAuth2 do provedor de identidade", "idpAuthUrl": "URL de Autorização", "idpAuthUrlDescription": "O URL do endpoint de autorização OAuth2", "idpTokenUrl": "URL do Token", "idpTokenUrlDescription": "O URL do endpoint do token OAuth2", "idpOidcConfigureAlert": "Informação Importante", - "idpOidcConfigureAlertDescription": "Após criar o provedor de identidade, será necessário configurar o URL de retorno nas configurações do seu provedor de identidade. O URL de retorno será fornecido após a criação bem-sucedida.", + "idpOidcConfigureAlertDescription": "Depois de criar o provedor de identidade, você precisará configurar o URL de retorno de chamada nas configurações do provedor de identidade. A URL de retorno de chamada será fornecida após a criação com sucesso.", "idpToken": "Configuração do Token", "idpTokenDescription": "Configurar como extrair informações do utilizador do token ID", "idpJmespathAbout": "Sobre JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Criar Provedor de Identidade", "orgPolicies": "Políticas da Organização", "idpSettings": "Configurações de {idpName}", - "idpCreateSettingsDescription": "Configurar as definições para o seu provedor de identidade", + "idpCreateSettingsDescription": "Configure as configurações para o provedor de identidade", "roleMapping": "Mapeamento de Funções", "orgMapping": "Mapeamento da Organização", "orgPoliciesSearch": "Pesquisar políticas da organização...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Provedor de identidade atualizado com sucesso", "redirectUrl": "URL de Redirecionamento", "redirectUrlAbout": "Sobre o URL de Redirecionamento", - "redirectUrlAboutDescription": "Este é o URL para o qual os utilizadores serão redirecionados após a autenticação. Precisa configurar este URL nas configurações do seu provedor de identidade.", + "redirectUrlAboutDescription": "Essa é a URL para a qual os usuários serão redirecionados após a autenticação. Você precisa configurar esta URL nas configurações do provedor de identidade.", "pangolinAuth": "Autenticação - Pangolin", "verificationCodeLengthRequirements": "O seu código de verificação deve ter 8 caracteres.", "errorOccurred": "Ocorreu um erro", @@ -909,6 +924,10 @@ "passwordResetSent": "Enviaremos um código de redefinição de palavra-passe para este email.", "passwordResetCode": "Código de Redefinição", "passwordResetCodeDescription": "Verifique o seu email para obter o código de redefinição.", + "generatePasswordResetCode": "Gerar código de redefinição de senha", + "passwordResetCodeGenerated": "Código de redefinição de senha gerado", + "passwordResetCodeGeneratedDescription": "Compartilhe este código com o usuário. Eles podem usá-lo para redefinir sua senha.", + "passwordResetUrl": "Reset URL", "passwordNew": "Nova Palavra-passe", "passwordNewConfirm": "Confirmar Nova Palavra-passe", "changePassword": "Mudar a senha", @@ -926,6 +945,9 @@ "pincodeAuth": "Código do Autenticador", "pincodeSubmit2": "Submeter Código", "passwordResetSubmit": "Solicitar Redefinição", + "passwordResetAlreadyHaveCode": "Digitar Código de Redefinição de Senha", + "passwordResetSmtpRequired": "Por favor, contate o administrador", + "passwordResetSmtpRequiredDescription": "É necessário um código de redefinição de senha para redefinir sua senha. Por favor, contate o administrador para assistência.", "passwordBack": "Voltar à Palavra-passe", "loginBack": "Voltar ao início de sessão", "signup": "Registar", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Listar Recursos do Site", "actionUpdateSiteResource": "Atualizar Recurso do Site", "actionListInvitations": "Listar Convites", + "actionExportLogs": "Exportar logs", + "actionViewLogs": "Visualizar registros", "noneSelected": "Nenhum selecionado", "orgNotFound2": "Nenhuma organização encontrada.", "searchProgress": "Pesquisar...", "create": "Criar", "orgs": "Organizações", "loginError": "Ocorreu um erro ao iniciar sessão", + "loginRequiredForDevice": "É necessário entrar para autenticar seu dispositivo.", "passwordForgot": "Esqueceu a sua palavra-passe?", "otpAuth": "Autenticação de Dois Fatores", "otpAuthDescription": "Insira o código da sua aplicação de autenticação ou um dos seus códigos de backup de uso único.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Residencial", "sidebarSites": "sites", "sidebarResources": "Recursos", + "sidebarProxyResources": "Público", + "sidebarClientResources": "Privado", "sidebarAccessControl": "Controle de Acesso", + "sidebarLogsAndAnalytics": "Registros e Análises", "sidebarUsers": "Utilizadores", + "sidebarAdmin": "Administrador", "sidebarInvitations": "Convites", "sidebarRoles": "Papéis", - "sidebarShareableLinks": "Links compartilháveis", + "sidebarShareableLinks": "Links", "sidebarApiKeys": "Chaves API", "sidebarSettings": "Configurações", "sidebarAllUsers": "Todos os utilizadores", "sidebarIdentityProviders": "Provedores de identidade", "sidebarLicense": "Tipo:", "sidebarClients": "Clientes", + "sidebarUserDevices": "Utilizadores", + "sidebarMachineClients": "Máquinas", "sidebarDomains": "Domínios", + "sidebarGeneral": "Gerais", + "sidebarLogAndAnalytics": "Registo & Análise", "sidebarBluePrints": "Diagramas", + "sidebarOrganization": "Organização", + "sidebarLogsAnalytics": "Análises", "blueprints": "Diagramas", "blueprintsDescription": "Aplicar configurações declarativas e ver execuções anteriores", "blueprintAdd": "Adicionar Diagrama", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Veja o resultado do diagrama aplicado e todos os erros que ocorreram", "blueprintInfo": "Informação do Diagrama", "message": "mensagem", - "blueprintContentsDescription": "Defina o conteúdo YAML descrevendo a sua infraestrutura", + "blueprintContentsDescription": "Definir o conteúdo YAML descrevendo a infraestrutura", "blueprintErrorCreateDescription": "Ocorreu um erro ao aplicar o diagrama", "blueprintErrorCreate": "Erro ao criar diagrama", "searchBlueprintProgress": "Pesquisar diagramas...", @@ -1230,15 +1265,15 @@ "loading": "Carregando", "restart": "Reiniciar", "domains": "Domínios", - "domainsDescription": "Gerir domínios para sua organização", + "domainsDescription": "Criar e gerenciar domínios disponíveis na organização", "domainsSearch": "Pesquisar domínios...", "domainAdd": "Adicionar Domínio", - "domainAddDescription": "Registre um novo domínio com sua organização", + "domainAddDescription": "Registrar um novo domínio com a organização", "domainCreate": "Criar Domínio", "domainCreatedDescription": "Domínio criado com sucesso", "domainDeletedDescription": "Domínio deletado com sucesso", - "domainQuestionRemove": "Tem certeza de que deseja remover o domínio da sua conta?", - "domainMessageRemove": "Uma vez removido, o domínio não estará mais associado à sua conta.", + "domainQuestionRemove": "Tem certeza de que deseja excluir o domínio?", + "domainMessageRemove": "Uma vez removido, o domínio não será mais associado à organização.", "domainConfirmDelete": "Confirmar Exclusão de Domínio", "domainDelete": "Excluir Domínio", "domain": "Domínio", @@ -1257,7 +1292,7 @@ "pending": "Pendente", "sidebarBilling": "Faturamento", "billing": "Faturamento", - "orgBillingDescription": "Gerir suas informações de faturação e assinaturas", + "orgBillingDescription": "Gerenciar informações e assinaturas de cobrança", "github": "GitHub", "pangolinHosted": "Hospedagem Pangolin", "fossorial": "Fossorial", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Atualizações de Produto", "productUpdateEmpty": "Não há atualizações", "dismissAll": "Recusar tudo", - "pangolinUpdateAvailable": "Nova versão disponível", + "pangolinUpdateAvailable": "Atualização disponível", "pangolinUpdateAvailableInfo": "A versão {version} está pronta para ser instalada", - "pangolinUpdateAvailableReleaseNotes": "Ver notas de lançamento", + "pangolinUpdateAvailableReleaseNotes": "Ver notas de versão", "newtUpdateAvailable": "Nova Atualização Disponível", "newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.", "domainPickerEnterDomain": "Domínio", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Verificando disponibilidade...", - "domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado. Tente um domínio diferente ou verifique as configurações do domínio da sua organização.", + "domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado. Tente um domínio diferente ou verifique as configurações do domínio da organização.", "domainPickerOrganizationDomains": "Domínios da Organização", "domainPickerProvidedDomains": "Domínios Fornecidos", "domainPickerSubdomain": "Subdomínio: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Modificar Assinatura", "billingStartSubscription": "Iniciar Assinatura", "billingRecurringCharge": "Cobrança Recorrente", - "billingManageSubscriptionSettings": "Gerenciar as configurações e preferências da sua assinatura", + "billingManageSubscriptionSettings": "Gerenciar configurações de assinatura e preferências", "billingNoActiveSubscription": "Você não tem uma assinatura ativa. Inicie sua assinatura para aumentar os limites de uso.", "billingFailedToLoadSubscription": "Falha ao carregar assinatura", "billingFailedToLoadUsage": "Falha ao carregar uso", @@ -1345,9 +1380,9 @@ "billingPortalError": "Erro do Portal", "billingDataUsageInfo": "Você é cobrado por todos os dados transferidos através de seus túneis seguros quando conectado à nuvem. Isso inclui o tráfego de entrada e saída em todos os seus sites. Quando você atingir o seu limite, seus sites desconectarão até que você atualize seu plano ou reduza o uso. Os dados não serão cobrados ao usar os nós.", "billingOnlineTimeInfo": "Cobrança de acordo com o tempo em que seus sites permanecem conectados à nuvem. Por exemplo, 44,640 minutos é igual a um site que roda 24/7 para um mês inteiro. Quando você atinge o seu limite, seus sites desconectarão até que você faça o upgrade do seu plano ou reduza o uso. O tempo não é cobrado ao usar nós.", - "billingUsersInfo": "Você será cobrado por cada usuário em sua organização. A cobrança é calculada diariamente com base no número de contas de usuário ativas em sua organização.", - "billingDomainInfo": "Você será cobrado por cada domínio em sua organização. A cobrança é calculada diariamente com base no número de contas de domínio ativas em sua organização.", - "billingRemoteExitNodesInfo": "Você será cobrado por cada Nodo gerenciado em sua organização. A cobrança é calculada diariamente com base no número de Nodos gerenciados ativos em sua organização.", + "billingUsersInfo": "A cobrança é feita por cada usuário na organização. A cobrança é feita diariamente com base no número de contas de usuário ativas na sua organização.", + "billingDomainInfo": "A cobrança é feita por cada domínio da organização. A cobrança é feita diariamente com base no número de contas de domínio ativas na sua organização.", + "billingRemoteExitNodesInfo": "Você é cobrado por cada nó gerenciado na organização. A cobrança é calculada diariamente com base no número de nós gerenciados ativos em sua organização.", "domainNotFound": "Domínio Não Encontrado", "domainNotFoundDescription": "Este recurso está desativado porque o domínio não existe mais em nosso sistema. Defina um novo domínio para este recurso.", "failed": "Falhou", @@ -1430,29 +1465,32 @@ "and": "e", "privacyPolicy": "política de privacidade" }, + "signUpMarketing": { + "keepMeInTheLoop": "Mantenha-me à disposição com notícias, atualizações e novos recursos por e-mail." + }, "siteRequired": "Site é obrigatório.", "olmTunnel": "Olm Tunnel", "olmTunnelDescription": "Use Olm para conectividade do cliente", "errorCreatingClient": "Erro ao criar cliente", "clientDefaultsNotFound": "Padrões do cliente não encontrados", "createClient": "Criar Cliente", - "createClientDescription": "Crie um novo cliente para conectar aos seus sites", + "createClientDescription": "Criar um novo cliente para acessar recursos privados", "seeAllClients": "Ver Todos os Clientes", "clientInformation": "Informações do Cliente", "clientNamePlaceholder": "Nome do cliente", "address": "Endereço", "subnetPlaceholder": "Sub-rede", - "addressDescription": "O endereço que este cliente usará para conectividade", + "addressDescription": "O endereço interno do cliente. Deve estar dentro da sub-rede da organização.", "selectSites": "Selecionar sites", "sitesDescription": "O cliente terá conectividade com os sites selecionados", "clientInstallOlm": "Instalar Olm", "clientInstallOlmDescription": "Execute o Olm em seu sistema", - "clientOlmCredentials": "Credenciais Olm", - "clientOlmCredentialsDescription": "É assim que Olm se autenticará com o servidor", - "olmEndpoint": "Endpoint Olm", - "olmId": "ID Olm", - "olmSecretKey": "Chave Secreta Olm", - "clientCredentialsSave": "Salve suas Credenciais", + "clientOlmCredentials": "Credenciais", + "clientOlmCredentialsDescription": "É assim que o cliente irá se autenticar com o servidor", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Chave Secreta", + "clientCredentialsSave": "Salvar as Credenciais", "clientCredentialsSaveDescription": "Você só poderá ver isto uma vez. Certifique-se de copiá-las para um local seguro.", "generalSettingsDescription": "Configure as configurações gerais para este cliente", "clientUpdated": "Cliente atualizado", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Ocorreu um erro ao buscar sites.", "olmErrorFetchReleases": "Ocorreu um erro ao buscar lançamentos do Olm.", "olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.", - "remoteSubnets": "Sub-redes Remotas", "enterCidrRange": "Insira o intervalo CIDR", - "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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Monitore a saúde deste alvo. Você pode monitorar um ponto de extremidade diferente do alvo, se necessário.", "healthScheme": "Método", "healthSelectScheme": "Selecione o Método", + "healthCheckPortInvalid": "A porta do exame de saúde deve estar entre 1 e 65535", "healthCheckPath": "Caminho", "healthHostname": "IP / Nome do Host", "healthPort": "Porta", "healthCheckPathDescription": "O caminho para verificar o estado de saúde.", - "healthyIntervalSeconds": "Intervalo Saudável", - "unhealthyIntervalSeconds": "Intervalo Não Saudável", + "healthyIntervalSeconds": "Intervalo Saudável (seg)", + "unhealthyIntervalSeconds": "Intervalo Insalubre (seg)", "IntervalSeconds": "Intervalo Saudável", - "timeoutSeconds": "Tempo Limite", + "timeoutSeconds": "Tempo limite (seg)", "timeIsInSeconds": "O tempo está em segundos", "retryAttempts": "Tentativas de Repetição", "expectedResponseCodes": "Códigos de Resposta Esperados", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Editar Domínio", "siteName": "Nome do Site", "proxyPort": "Porta", - "resourcesTableProxyResources": "Recursos de Proxy", - "resourcesTableClientResources": "Recursos do Cliente", + "resourcesTableProxyResources": "Público", + "resourcesTableClientResources": "Privado", "resourcesTableNoProxyResourcesFound": "Nenhum recurso de proxy encontrado.", "resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.", "resourcesTableDestination": "Destino", - "resourcesTableTheseResourcesForUseWith": "Esses recursos são para uso com", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Clientes", "resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.", "resourcesTableNoTargets": "Nenhum alvo", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Desconectado", "resourcesTableUnknown": "Desconhecido", "resourcesTableNotMonitored": "Não monitorado", - "editInternalResourceDialogEditClientResource": "Editar Recurso do Cliente", - "editInternalResourceDialogUpdateResourceProperties": "Atualize as propriedades do recurso e a configuração do alvo para {resourceName}.", + "editInternalResourceDialogEditClientResource": "Editar Recurso Privado", + "editInternalResourceDialogUpdateResourceProperties": "Atualizar as configurações de recursos e controles de acesso para {resourceName}", "editInternalResourceDialogResourceProperties": "Propriedades do Recurso", "editInternalResourceDialogName": "Nome", "editInternalResourceDialogProtocol": "Protocolo", @@ -1560,11 +1597,22 @@ "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", + "editInternalResourceDialogPortModeRequired": "Protocolo, porta de proxy e porta de destino são necessários para o modo de porto", + "editInternalResourceDialogMode": "Modo", + "editInternalResourceDialogModePort": "Porta", + "editInternalResourceDialogModeHost": "Servidor", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Destino", + "editInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.", + "editInternalResourceDialogDestinationIPDescription": "O IP ou endereço do hostname do recurso na rede do site.", + "editInternalResourceDialogDestinationCidrDescription": "A faixa CIDR do recurso na rede do site.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Um alias de DNS interno opcional para este recurso.", "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.", + "createInternalResourceDialogCreateClientResource": "Criar Recurso Privado", + "createInternalResourceDialogCreateClientResourceDescription": "Criar um novo recurso que só será acessível para clientes conectados à organização", "createInternalResourceDialogResourceProperties": "Propriedades do Recurso", "createInternalResourceDialogName": "Nome", "createInternalResourceDialogSite": "Site", @@ -1593,11 +1641,22 @@ "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", + "createInternalResourceDialogPortModeRequired": "Protocolo, porta de proxy e porta de destino são necessários para o modo de porto", + "createInternalResourceDialogMode": "Modo", + "createInternalResourceDialogModePort": "Porta", + "createInternalResourceDialogModeHost": "Servidor", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Destino", + "createInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.", + "createInternalResourceDialogDestinationCidrDescription": "A faixa CIDR do recurso na rede do site.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Um alias de DNS interno opcional para este recurso.", "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.", + "siteAcceptClientConnectionsDescription": "Permitir que dispositivos de usuário e clientes acessem recursos neste site. Isso pode ser alterado mais tarde.", + "siteAddress": "Endereço do Site (Avançado)", + "siteAddressDescription": "Endereço interno do site. Deve estar dentro da sub-rede da organização.", + "siteNameDescription": "O nome de exibição do site que pode ser alterado mais tarde.", "autoLoginExternalIdp": "Login Automático com IDP Externo", "autoLoginExternalIdpDescription": "Redirecionar imediatamente o utilizador para o IDP externo para autenticação.", "selectIdp": "Selecionar IDP", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.", "autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.", "remoteExitNodeManageRemoteExitNodes": "Nós remotos", - "remoteExitNodeDescription": "Auto-hospedar um ou mais nós remotos para estender sua conectividade de rede e reduzir a dependência da nuvem", + "remoteExitNodeDescription": "Auto-hospedar um ou mais nós remotos para estender a conectividade de rede e reduzir a dependência da nuvem", "remoteExitNodes": "Nós", "searchRemoteExitNodes": "Buscar nós...", "remoteExitNodeAdd": "Adicionar node", @@ -1623,11 +1682,11 @@ "sidebarRemoteExitNodes": "Nós remotos", "remoteExitNodeCreate": { "title": "Criar nó", - "description": "Crie um novo nó para estender sua conectividade de rede", + "description": "Crie um novo nó para estender a conectividade de rede", "viewAllButton": "Ver Todos os Nós", "strategy": { "title": "Estratégia de Criação", - "description": "Escolha isto para configurar o seu nó manualmente ou gerar novas credenciais.", + "description": "Escolha esta opção para configurar o nó manualmente ou gerar novas credenciais.", "adopt": { "title": "Adotar Nodo", "description": "Escolha isto se você já tem credenciais para o nó." @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Credenciais Geradas", - "description": "Use estas credenciais geradas para configurar o seu nó", + "description": "Use estas credenciais geradas para configurar o nó", "nodeIdTitle": "Nó ID", "secretTitle": "Chave Secreta", "saveCredentialsTitle": "Adicionar Credenciais à Configuração", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "Tipo de provedor de identidade", "roleMappingExpressionPlaceholder": "ex.: Contem (grupos, 'administrador') && 'Administrador' 「'Membro'", "idpGoogleConfiguration": "Configuração do Google", - "idpGoogleConfigurationDescription": "Configurar suas credenciais do Google OAuth2", - "idpGoogleClientIdDescription": "Seu ID de Cliente OAuth2 do Google", - "idpGoogleClientSecretDescription": "Seu Segredo de Cliente OAuth2 do Google", + "idpGoogleConfigurationDescription": "Configurar as credenciais do Google OAuth2", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Segredo de cliente OAuth2 do Google", "idpAzureConfiguration": "Configuração de ID do Azure Entra", - "idpAzureConfigurationDescription": "Configure as suas credenciais do Azure Entra ID OAuth2", + "idpAzureConfigurationDescription": "Configurar credenciais do Azure Entra ID OAuth2", "idpTenantId": "ID do Inquilino", - "idpTenantIdPlaceholder": "seu-tenente-id", - "idpAzureTenantIdDescription": "Seu ID do tenant Azure (encontrado na visão geral do diretório ativo Azure)", - "idpAzureClientIdDescription": "Seu ID de Cliente de Registro do App Azure", - "idpAzureClientSecretDescription": "Seu segredo de cliente de registro de aplicativos Azure", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "ID do tenant Azure (encontrado na visão geral do diretório ativo Azure)", + "idpAzureClientIdDescription": "ID cliente de registro do aplicativo Azure", + "idpAzureClientSecretDescription": "Segredo cliente de registro do Azure App", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Configuração do Google", "idpAzureConfigurationTitle": "Configuração de ID do Azure Entra", "idpTenantIdLabel": "ID do Inquilino", - "idpAzureClientIdDescription2": "Seu ID de Cliente de Registro do App Azure", - "idpAzureClientSecretDescription2": "Seu segredo de cliente de registro de aplicativos Azure", + "idpAzureClientIdDescription2": "ID cliente de registro do aplicativo Azure", + "idpAzureClientSecretDescription2": "Segredo cliente de registro do Azure App", "idpGoogleDescription": "Provedor Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Sub-rede", "subnetDescription": "A sub-rede para a configuração de rede dessa organização.", "authPage": "Página de Autenticação", - "authPageDescription": "Configurar a página de autenticação para sua organização", + "authPageDescription": "Configurar a página de autenticação para a organização", "authPageDomain": "Domínio de Página Autenticação", "noDomainSet": "Nenhum domínio definido", "changeDomain": "Alterar domínio", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Definir domínio da página de autenticação", "failedToFetchCertificate": "Falha ao buscar o certificado", "failedToRestartCertificate": "Falha ao reiniciar o certificado", - "addDomainToEnableCustomAuthPages": "Adicione um domínio para habilitar páginas de autenticação personalizadas para sua organização", + "addDomainToEnableCustomAuthPages": "Adicione um domínio para habilitar páginas de autenticação personalizadas para a organização", "selectDomainForOrgAuthPage": "Selecione um domínio para a página de autenticação da organização", "domainPickerProvidedDomain": "Domínio fornecido", "domainPickerFreeProvidedDomain": "Domínio fornecido grátis", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" não pôde ser válido para {domain}.", "domainPickerSubdomainSanitized": "Subdomínio banalizado", "domainPickerSubdomainCorrected": "\"{sub}\" foi corrigido para \"{sanitized}\"", - "orgAuthSignInTitle": "Entrar na sua organização", + "orgAuthSignInTitle": "Fazer login na organização", "orgAuthChooseIdpDescription": "Escolha o seu provedor de identidade para continuar", "orgAuthNoIdpConfigured": "Esta organização não tem nenhum provedor de identidade configurado. Você pode entrar com a identidade do seu Pangolin.", "orgAuthSignInWithPangolin": "Entrar com o Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Ativar autenticação de dois fatores", "completeSecuritySteps": "Passos de segurança completos", "securitySettings": "Configurações de Segurança", - "securitySettingsDescription": "Configurar políticas de segurança para a sua organização", + "securitySettingsDescription": "Configurar políticas de segurança para a organização", "requireTwoFactorForAllUsers": "Exigir autenticação dupla para todos os usuários", "requireTwoFactorDescription": "Quando ativado, todos os usuários internos nesta organização devem ter a autenticação de dois fatores ativada para acessar a organização.", "requireTwoFactorDisabledDescription": "Este recurso requer uma licença válida (Enterprise) ou assinatura ativa (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Edição Enterprise", "unlicensed": "Sem licença", "beta": "Beta", - "manageClients": "Gerenciar Clientes", - "manageClientsDescription": "Clientes são dispositivos que podem se conectar aos seus sites", + "manageUserDevices": "Dispositivos do usuário", + "manageUserDevicesDescription": "Ver e gerenciar dispositivos que os usuários usam para se conectar de forma privada aos recursos", + "manageMachineClients": "Gerenciar Clientes de Máquina", + "manageMachineClientsDescription": "Crie e gerencie clientes que servidores e sistemas usam para se conectar de forma privada aos recursos", + "clientsTableUserClients": "Utilizador", + "clientsTableMachineClients": "Máquina", "licenseTableValidUntil": "Válido até", "saasLicenseKeysSettingsTitle": "Licenças empresariais", "saasLicenseKeysSettingsDescription": "Gerar e gerenciar chaves de licença Enterprise para instâncias Pangolin auto-hospedadas", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "faixa", "sidebarEnableEnterpriseLicense": "Habilitar Licença Empresarial", "cannotbeUndone": "Isso não pode ser desfeito.", - "toConfirm": "para confirmar", + "toConfirm": "para confirmar.", "deleteClientQuestion": "Você tem certeza que deseja remover o cliente do site e da organização?", "clientMessageRemove": "Depois de removido, o cliente não poderá mais se conectar ao site.", "sidebarLogs": "Registros", "request": "Pedir", + "requests": "Solicitações", "logs": "Registros", "logsSettingsDescription": "Monitorar logs coletados desta orginização", "searchLogs": "Pesquisar registros...", @@ -2020,6 +2084,7 @@ "ip": "PI", "reason": "Motivo", "requestLogs": "Registro de pedidos", + "requestAnalytics": "Solicitar análise", "host": "Servidor", "location": "Local:", "actionLogs": "Logs de Ações", @@ -2029,6 +2094,7 @@ "logRetention": "Retenção de Log", "logRetentionDescription": "Gerenciar quanto tempo os diferentes tipos de logs são mantidos para esta organização ou desativá-los", "requestLogsDescription": "Ver registros de pedidos detalhados de recursos nesta organização", + "requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização", "logRetentionRequestLabel": "Solicitar retenção de registro", "logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos", "logRetentionAccessLabel": "Retenção de Log de Acesso", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 dias", "logRetention90Days": "90 dias", "logRetentionForever": "Permanentemente", + "logRetentionEndOfFollowingYear": "Fim do ano seguinte", "actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização", "accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização", "licenseRequiredToUse": "É necessária uma licença empresarial para usar esse recurso.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Prefere Certificado Wildcard", "unverified": "Não verificado", "domainSetting": "Configurações do domínio", - "domainSettingDescription": "Configure as configurações para o seu domínio", + "domainSettingDescription": "Configurar configurações para o domínio", "preferWildcardCertDescription": "Tentativa de gerar um certificado coringa (requer um resolvedor de certificado devidamente configurado).", "recordName": "Nome da gravação", "auto": "Automático", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Uma versão atualizada do Olm está disponível. Atualize para a versão mais recente para ter a melhor experiência.", "client": "Cliente", "proxyProtocol": "Configurações de Protocolo Proxy", - "proxyProtocolDescription": "Configurar o protocolo Proxy para preservar endereços IP do cliente para serviços TCP/UDP.", + "proxyProtocolDescription": "Configurar o protocolo proxy para preservar endereços IP do cliente para serviços TCP.", "enableProxyProtocol": "Habilitar protocolo proxy", - "proxyProtocolInfo": "Preservar endereços IP do cliente para backends TCP/UDP", + "proxyProtocolInfo": "Preservar endereços IP do cliente para backends TCP", "proxyProtocolVersion": "Versão do Protocolo Proxy", "version1": " Versão 1 (recomendado)", "version2": "Versão 2", "versionDescription": "A versão 1 é baseada em texto e amplamente suportada. A versão 2 é binária e mais eficiente, mas menos compatível.", "warning": "ATENÇÃO", - "proxyProtocolWarning": "Seu aplicativo de backend deve ser configurado para aceitar conexões de protocolo de proxy. Se o seu backend não suportar o protocolo de protocolo, habilitando isso quebrará todas as conexões. Certifique-se de configurar seu backend para confiar nos cabeçalhos do protocolo proxy no Traefik.", + "proxyProtocolWarning": "A aplicação de backend deve ser configurada para aceitar conexões de protocolo proxy. Se o seu backend não suporta o Protocolo de Proxy, habilitando isto quebrará todas as conexões, então só habilite isso se você souber o que está fazendo. Certifique-se de configurar seu backend para confiar nos cabeçalhos do protocolo proxy no Traefik.", "restarting": "Reiniciando...", "manual": "Manualmente", "messageSupport": "Suporte a Mensagens", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Mensagem enviada!", "supportWillContact": "Entraremos em contato em breve!", "selectLogRetention": "Selecionar retenção de log", + "terms": "Termos", + "privacy": "Privacidade", + "security": "Segurança", + "docs": "Documentação", + "deviceActivation": "Ativação do dispositivo", + "deviceCodeInvalidFormat": "O código deve ter 9 caracteres (ex.: A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Código inválido ou expirado", + "deviceCodeVerifyFailed": "Falha ao verificar o código do dispositivo", + "signedInAs": "Sessão iniciada como", + "deviceCodeEnterPrompt": "Digite o código exibido no dispositivo", + "continue": "Continuar", + "deviceUnknownLocation": "Localização desconhecida", + "deviceAuthorizationRequested": "Esta autorização foi solicitada por {location} no {date}. Certifique-se de que você confia neste dispositivo, pois ele terá acesso à conta.", + "deviceLabel": "Dispositivo: {deviceName}", + "deviceWantsAccess": "quer acessar sua conta", + "deviceExistingAccess": "Acesso existente:", + "deviceFullAccess": "Acesso total à sua conta", + "deviceOrganizationsAccess": "Acesso a todas as organizações que sua conta tem acesso a", + "deviceAuthorize": "Autorizar {applicationName}", + "deviceConnected": "Dispositivo Conectado!", + "deviceAuthorizedMessage": "O dispositivo está autorizado a acessar sua conta.", + "pangolinCloud": "Nuvem do Pangolin", + "viewDevices": "Ver Dispositivos", + "viewDevicesDescription": "Gerencie seus dispositivos conectados", + "noDevices": "Nenhum dispositivo encontrado", + "dateCreated": "Data de Criação", + "unnamedDevice": "Dispositivo sem nome", + "deviceQuestionRemove": "Você tem certeza que deseja excluir este dispositivo?", + "deviceMessageRemove": "Esta ação não pode ser desfeita.", + "deviceDeleteConfirm": "Excluir dispositivo", + "deleteDevice": "Excluir dispositivo", + "errorLoadingDevices": "Erro ao carregar dispositivos", + "failedToLoadDevices": "Falha ao carregar dispositivos", + "deviceDeleted": "Dispositivo excluído", + "deviceDeletedDescription": "O dispositivo foi excluído com sucesso.", + "errorDeletingDevice": "Erro ao excluir dispositivo", + "failedToDeleteDevice": "Falha ao excluir dispositivo", "showColumns": "Exibir Colunas", "hideColumns": "Ocultar colunas", "columnVisibility": "Visibilidade da Coluna", @@ -2111,10 +2215,14 @@ "enableSelected": "Habilitar Selecionados", "disableSelected": "Desativar Selecionados", "checkSelectedStatus": "Status de Verificação dos Selecionados", + "clients": "Clientes", + "accessClientSelect": "Selecionar clientes de máquina", + "resourceClientDescription": "Clientes de máquina que podem acessar este recurso", + "regenerate": "Regenerar", "credentials": "Credenciais", "savecredentials": "Salvar Credenciais", - "regeneratecredentials": "Rechave", - "regenerateCredentials": "Regenerar e salvar suas credenciais", + "regenerateCredentialsButton": "Regerar Credenciais", + "regenerateCredentials": "Regerar Credenciais", "generatedcredentials": "Credenciais Geradas", "copyandsavethesecredentials": "Copiar e salvar estas credenciais", "copyandsavethesecredentialsdescription": "Essas credenciais não serão exibidas novamente depois que você sair desta página. Salve elas com segurança agora.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "As credenciais foram regeneradas e salvas com sucesso.", "credentialsSaveError": "Erro ao Salvar Credenciais", "credentialsSaveErrorDescription": "Ocorreu um erro enquanto regenerava e salvava as credenciais.", - "regenerateCredentialsWarning": "Regenerar credenciais irá invalidar as anteriores. Certifique-se de atualizar qualquer configuração que use essas credenciais.", + "regenerateCredentialsWarning": "Regenerar credenciais irá invalidar as anteriores e causar uma desconexão. Certifique-se de atualizar quaisquer configurações que usam essas credenciais.", "confirm": "Confirmar", "regenerateCredentialsConfirmation": "Você tem certeza que deseja recriar as credenciais?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Chave secreta", - "featureDisabledTooltip": "Este recurso só está disponível no plano corporativo e requer que uma licença utilize.", "niceId": "Belo ID", "niceIdUpdated": "Bom ID atualizado", "niceIdUpdatedSuccessfully": "Bom ID atualizado com sucesso", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Ocorreu um erro ao atualizar a ID de Nice.", "niceIdCannotBeEmpty": "Bom ID não pode estar vazio", "enterIdentifier": "Inserir identificador", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "Não é você? Use uma conta diferente.", + "deviceLoginDeviceRequestingAccessToAccount": "Um dispositivo está solicitando acesso a essa conta.", + "noData": "Nenhum dado encontrado", + "machineClients": "Clientes de máquina", + "install": "Instale", + "run": "Executar", + "clientNameDescription": "O nome de exibição do cliente que pode ser alterado mais tarde.", + "clientAddress": "Endereço do Cliente (Avançado)", + "setupFailedToFetchSubnet": "Falha ao buscar a subrede padrão", + "setupSubnetAdvanced": "Sub-rede (Avançado)", + "setupSubnetDescription": "A sub-rede para a rede interna desta organização.", + "siteRegenerateAndDisconnect": "Regerar e Desconectar", + "siteRegenerateAndDisconnectConfirmation": "Você tem certeza que deseja regenerar as credenciais e desconectar este site?", + "siteRegenerateAndDisconnectWarning": "Isto irá regenerar as credenciais e desconectar imediatamente o site. O site precisará ser reiniciado com as novas credenciais.", + "siteRegenerateCredentialsConfirmation": "Você tem certeza que deseja regenerar as credenciais para este site?", + "siteRegenerateCredentialsWarning": "Isso irá regenerar as credenciais. O site permanecerá conectado até que você reinicie-o manualmente e use as novas credenciais.", + "clientRegenerateAndDisconnect": "Regerar e Desconectar", + "clientRegenerateAndDisconnectConfirmation": "Tem certeza que deseja regenerar as credenciais e desconectar este cliente?", + "clientRegenerateAndDisconnectWarning": "Isto irá regenerar as credenciais e desconectar o cliente imediatamente. O cliente precisará ser reiniciado com as novas credenciais.", + "clientRegenerateCredentialsConfirmation": "Tem certeza que deseja regenerar as credenciais para este cliente?", + "clientRegenerateCredentialsWarning": "Isto irá regenerar as credenciais. O cliente permanecerá conectado até você reiniciá-lo manualmente e usar as novas credenciais.", + "remoteExitNodeRegenerateAndDisconnect": "Regerar e Desconectar", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Tem certeza que deseja regenerar as credenciais e desconectar este nó de saída remota?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Isto irá regenerar as credenciais e desconectar imediatamente o nó de saída remota. O nó de saída remota precisará ser reiniciado com as novas credenciais.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Você tem certeza que deseja regenerar as credenciais para este nó de saída remota?", + "remoteExitNodeRegenerateCredentialsWarning": "Isto irá regenerar as credenciais. O nó de saída remota permanecerá conectado até que você o reinicie manualmente e use as novas credenciais.", + "agent": "Representante" } diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 23c521c1..d687b783 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -1,12 +1,12 @@ { - "setupCreate": "Создайте свою организацию, сайт и ресурсы", + "setupCreate": "Создать организацию, сайт и ресурсы", "setupNewOrg": "Новая организация", "setupCreateOrg": "Создать организацию", "setupCreateResources": "Создать ресурсы", "setupOrgName": "Название организации", - "orgDisplayName": "Это отображаемое имя вашей организации.", + "orgDisplayName": "Отображаемое имя организации.", "orgId": "ID организации", - "setupIdentifierMessage": "Уникальный идентификатор вашей организации. Он задаётся отдельно от отображаемого имени.", + "setupIdentifierMessage": "Это уникальный идентификатор для организации.", "setupErrorIdentifier": "ID организации уже занят. Выберите другой.", "componentsErrorNoMemberCreate": "Вы пока не состоите ни в одной организации. Создайте организацию для начала работы.", "componentsErrorNoMember": "Вы пока не состоите ни в одной организации.", @@ -50,10 +50,10 @@ "siteMessageRemove": "После удаления сайт больше не будет доступен. Все цели, связанные с сайтом, также будут удалены.", "siteQuestionRemove": "Вы уверены, что хотите удалить сайт из организации?", "siteManageSites": "Управление сайтами", - "siteDescription": "Обеспечьте подключение к вашей сети через защищённые туннели", + "siteDescription": "Создание и управление сайтами, чтобы включить подключение к приватным сетям", "siteCreate": "Создать сайт", "siteCreateDescription2": "Следуйте инструкциям ниже для создания и подключения нового сайта", - "siteCreateDescription": "Создайте новый сайт для подключения ваших ресурсов", + "siteCreateDescription": "Создайте новый сайт для начала подключения ресурсов", "close": "Закрыть", "siteErrorCreate": "Ошибка при создании сайта", "siteErrorCreateKeyPair": "Пара ключей или настройки сайта по умолчанию не найдены", @@ -74,7 +74,7 @@ "siteInstallNewt": "Установить Newt", "siteInstallNewtDescription": "Запустите Newt в вашей системе", "WgConfiguration": "Конфигурация WireGuard", - "WgConfigurationDescription": "Используйте следующую конфигурацию для подключения к вашей сети", + "WgConfigurationDescription": "Используйте следующую конфигурацию для подключения к сети", "operatingSystem": "Операционная система", "commands": "Команды", "recommended": "Рекомендуется", @@ -87,32 +87,32 @@ "siteUpdated": "Сайт обновлён", "siteUpdatedDescription": "Сайт был успешно обновлён.", "siteGeneralDescription": "Настройте общие параметры для этого сайта", - "siteSettingDescription": "Настройте параметры вашего сайта", + "siteSettingDescription": "Настройка параметров на сайте", "siteSetting": "Настройки {siteName}", - "siteNewtTunnel": "Туннель Newt (Рекомендуется)", - "siteNewtTunnelDescription": "Простейший способ создать точку входа в вашу сеть. Дополнительная настройка не требуется.", + "siteNewtTunnel": "Новый сайт (рекомендуется)", + "siteNewtTunnelDescription": "Самый простой способ создать точку входа в любую сеть. Дополнительная настройка не требуется.", "siteWg": "Базовый WireGuard", "siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.", "siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", "siteLocalDescription": "Только локальные ресурсы. Без туннелирования.", "siteLocalDescriptionSaas": "Только локальные ресурсы. Нет туннелей. Только для удаленных узлов.", "siteSeeAll": "Просмотреть все сайты", - "siteTunnelDescription": "Выберите способ подключения к вашему сайту", - "siteNewtCredentials": "Учётные данные Newt", - "siteNewtCredentialsDescription": "Так Newt будет выполнять аутентификацию на сервере", - "siteCredentialsSave": "Сохраните ваши учётные данные", + "siteTunnelDescription": "Определите, как вы хотите подключиться к сайту", + "siteNewtCredentials": "Полномочия", + "siteNewtCredentialsDescription": "Вот как сайт будет аутентифицироваться с сервером", + "siteCredentialsSave": "Сохранить учетные данные", "siteCredentialsSaveDescription": "Вы сможете увидеть эти данные только один раз. Обязательно скопируйте их в безопасное место.", "siteInfo": "Информация о сайте", "status": "Статус", "shareTitle": "Управление общими ссылками", - "shareDescription": "Создавайте общие ссылки для предоставления временного или постоянного доступа к вашим ресурсам", + "shareDescription": "Создавайте общие ссылки, чтобы предоставить временный или постоянный доступ к прокси ресурсам", "shareSearch": "Поиск общих ссылок...", "shareCreate": "Создать общую ссылку", "shareErrorDelete": "Не удалось удалить ссылку", "shareErrorDeleteMessage": "Произошла ошибка при удалении ссылки", "shareDeleted": "Ссылка удалена", "shareDeletedDescription": "Ссылка была успешно удалена", - "shareTokenDescription": "Ваш токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Он должен передаваться клиентом при каждом запросе для аутентификации.", + "shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.", "accessToken": "Токен доступа", "usageExamples": "Примеры использования", "tokenId": "ID токена", @@ -121,7 +121,7 @@ "importantNote": "Важное примечание", "shareImportantDescription": "Из соображений безопасности рекомендуется использовать заголовки вместо параметров запроса, когда это возможно, так как параметры запроса могут сохраняться в логах сервера или истории браузера.", "token": "Токен", - "shareTokenSecurety": "Храните ваш токен доступа в безопасности. Не делитесь им в общедоступных местах или клиентском коде.", + "shareTokenSecurety": "Храните токен доступа в безопасном режиме. Не делитесь им в общедоступных областях или на клиентской стороне.", "shareErrorFetchResource": "Не удалось получить ресурсы", "shareErrorFetchResourceDescription": "Произошла ошибка при получении ресурсов", "shareErrorCreate": "Не удалось создать общую ссылку", @@ -144,8 +144,10 @@ "expires": "Истекает", "never": "Никогда", "shareErrorSelectResource": "Пожалуйста, выберите ресурс", - "resourceTitle": "Управление ресурсами", - "resourceDescription": "Создавайте защищённые прокси к вашим приватным приложениям", + "proxyResourceTitle": "Управление публичными ресурсами", + "proxyResourceDescription": "Создание и управление ресурсами, которые доступны через веб-браузер", + "clientResourceTitle": "Управление приватными ресурсами", + "clientResourceDescription": "Создание и управление ресурсами, которые доступны только через подключенный клиент", "resourcesSearch": "Поиск ресурсов...", "resourceAdd": "Добавить ресурс", "resourceErrorDelte": "Ошибка при удалении ресурса", @@ -155,9 +157,9 @@ "resourceMessageRemove": "После удаления ресурс больше не будет доступен. Все целевые узлы, связанные с ресурсом, также будут удалены.", "resourceQuestionRemove": "Вы уверены, что хотите удалить ресурс из организации?", "resourceHTTP": "HTTPS-ресурс", - "resourceHTTPDescription": "Проксирование запросов к вашему приложению через HTTPS с использованием поддомена или базового домена.", + "resourceHTTPDescription": "Прокси-запросы к приложению по HTTPS с помощью поддомена или базового домена.", "resourceRaw": "Сырой TCP/UDP-ресурс", - "resourceRawDescription": "Проксирование запросов к вашему приложению через TCP/UDP с использованием по номеру порта.", + "resourceRawDescription": "Прокси запрашивает приложение через TCP/UDP по номеру порта. Это работает только тогда, когда сайты подключены к узлам.", "resourceCreate": "Создание ресурса", "resourceCreateDescription": "Следуйте инструкциям ниже для создания нового ресурса", "resourceSeeAll": "Посмотреть все ресурсы", @@ -171,22 +173,22 @@ "noCountryFound": "Страна не найдена.", "siteSelectionDescription": "Этот сайт предоставит подключение к цели.", "resourceType": "Тип ресурса", - "resourceTypeDescription": "Определите, как вы хотите получать доступ к вашему ресурсу", + "resourceTypeDescription": "Определить как получить доступ к ресурсу", "resourceHTTPSSettings": "Настройки HTTPS", - "resourceHTTPSSettingsDescription": "Настройте, как будет осуществляться доступ к вашему ресурсу через HTTPS", + "resourceHTTPSSettingsDescription": "Настройка доступа к ресурсу по HTTPS", "domainType": "Тип домена", "subdomain": "Поддомен", "baseDomain": "Базовый домен", - "subdomnainDescription": "Поддомен, на котором будет доступен ресурс.", + "subdomnainDescription": "Поддомен, в котором ресурс будет доступен.", "resourceRawSettings": "Настройки TCP/UDP", - "resourceRawSettingsDescription": "Настройте доступ к вашему ресурсу по TCP/UDP. Вы соотносите ресурс с портом на сервере хоста Pangolin, так что вы можете получить доступ к ресурсу с сервера server-public-ip:mapped-порта.", + "resourceRawSettingsDescription": "Настройка доступа к ресурсу по TCP/UDP", "protocol": "Протокол", "protocolSelect": "Выберите протокол", "resourcePortNumber": "Номер порта", "resourcePortNumberDescription": "Внешний номер порта для проксирования запросов.", "cancel": "Отмена", "resourceConfig": "Фрагменты конфигурации", - "resourceConfigDescription": "Скопируйте и вставьте эти фрагменты конфигурации для настройки вашего TCP/UDP-ресурса", + "resourceConfigDescription": "Скопируйте и вставьте эти сниппеты для настройки TCP/UDP ресурса", "resourceAddEntrypoints": "Traefik: Добавить точки входа", "resourceExposePorts": "Gerbil: Открыть порты в Docker Compose", "resourceLearnRaw": "Узнайте, как настроить TCP/UDP-ресурсы", @@ -202,14 +204,14 @@ "proxy": "Прокси", "internal": "Внутренний", "rules": "Правила", - "resourceSettingDescription": "Настройте параметры вашего ресурса", + "resourceSettingDescription": "Настройка параметров ресурса", "resourceSetting": "Настройки {resourceName}", - "alwaysAllow": "Всегда разрешать", - "alwaysDeny": "Всегда запрещать", + "alwaysAllow": "Авторизация байпасса", + "alwaysDeny": "Блокировать доступ", "passToAuth": "Переход к аутентификации", - "orgSettingsDescription": "Настройте общие параметры вашей организации", + "orgSettingsDescription": "Настроить настройки организации", "orgGeneralSettings": "Настройки организации", - "orgGeneralSettingsDescription": "Управляйте данными и конфигурацией вашей организации", + "orgGeneralSettingsDescription": "Управление деталями и конфигурацией организации", "saveGeneralSettings": "Сохранить общие настройки", "saveSettings": "Сохранить настройки", "orgDangerZone": "Опасная зона", @@ -232,7 +234,7 @@ "orgMissing": "Отсутствует ID организации", "orgMissingMessage": "Невозможно восстановить приглашение без ID организации.", "accessUsersManage": "Управление пользователями", - "accessUsersDescription": "Приглашайте пользователей и назначайте им роли для управления доступом к вашей организации", + "accessUsersDescription": "Пригласить и управлять пользователями с доступом к этой организации", "accessUsersSearch": "Поиск пользователей...", "accessUserCreate": "Создать пользователя", "accessUserRemove": "Удалить пользователя", @@ -241,13 +243,13 @@ "role": "Роль", "nameRequired": "Имя обязательно", "accessRolesManage": "Управление ролями", - "accessRolesDescription": "Настройте роли для управления доступом к вашей организации", + "accessRolesDescription": "Создание и управление ролями для пользователей в организации", "accessRolesSearch": "Поиск ролей...", "accessRolesAdd": "Добавить роль", "accessRoleDelete": "Удалить роль", "description": "Описание", "inviteTitle": "Открытые приглашения", - "inviteDescription": "Управляйте вашими приглашениями для других пользователей", + "inviteDescription": "Управление приглашениями для присоединения других пользователей к организации", "inviteSearch": "Поиск приглашений...", "minutes": "мин.", "hours": "ч.", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "Ошибка при создании ключа API", "apiKeysErrorSetPermission": "Ошибка при установке разрешений", "apiKeysCreate": "Сгенерировать ключ API", - "apiKeysCreateDescription": "Сгенерируйте новый ключ API для вашей организации", + "apiKeysCreateDescription": "Сгенерировать новый ключ API для организации", "apiKeysGeneralSettings": "Разрешения", "apiKeysGeneralSettingsDescription": "Определите, что может делать этот ключ API", - "apiKeysList": "Ваш ключ API", - "apiKeysSave": "Сохраните ваш ключ API", + "apiKeysList": "Новый ключ API", + "apiKeysSave": "Сохранить ключ API", "apiKeysSaveDescription": "Вы сможете увидеть этот ключ только один раз. Обязательно скопируйте его в безопасное место.", - "apiKeysInfo": "Ваш ключ API:", + "apiKeysInfo": "Ключ API:", "apiKeysConfirmCopy": "Я скопировал(а) ключ API", "generate": "Сгенерировать", "done": "Готово", @@ -424,7 +426,7 @@ "userCreated": "Пользователь создан", "userCreatedDescription": "Пользователь был успешно создан.", "userTypeInternal": "Внутренний пользователь", - "userTypeInternalDescription": "Пригласите пользователя напрямую в вашу организацию.", + "userTypeInternalDescription": "Пригласить пользователя присоединиться к организации напрямую.", "userTypeExternal": "Внешний пользователь", "userTypeExternalDescription": "Создайте пользователя через внешний Identity Provider.", "accessUserCreateDescription": "Следуйте инструкциям ниже для создания нового пользователя", @@ -436,6 +438,16 @@ "inviteEmailSent": "Отправить приглашение по Email", "inviteValid": "Действительно", "selectDuration": "Укажите срок действия", + "selectResource": "Выберите ресурс", + "filterByResource": "Фильтровать по ресурсам", + "resetFilters": "Сбросить фильтры", + "totalBlocked": "Запросы заблокированы Панголином", + "totalRequests": "Всего запросов", + "requestsByCountry": "Запросы по стране", + "requestsByDay": "Запросы по дням", + "blocked": "Заблокирован", + "allowed": "Разрешено", + "topCountries": "Лучшие страны", "accessRoleSelect": "Выберите роль", "inviteEmailSentDescription": "Email был отправлен пользователю со ссылкой доступа ниже. Он должен перейти по ссылке для принятия приглашения.", "inviteSentDescription": "Пользователь был приглашён. Он должен перейти по ссылке ниже для принятия приглашения.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Сохранить контроль доступа", "roles": "Роли", "accessUsersRoles": "Управление пользователями и ролями", - "accessUsersRolesDescription": "Приглашайте пользователей и добавляйте их в роли для управления доступом к вашей организации", + "accessUsersRolesDescription": "Пригласить пользователей и добавить их в роли для управления доступом к организации", "key": "Ключ", "createdAt": "Создано в", "proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.", "proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.", "proxyEnableSSL": "Включить SSL", - "proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS подключений к вашим целям.", + "proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS соединений с целями.", "target": "Target", "configureTarget": "Настроить адресаты", "targetErrorFetch": "Не удалось получить цели", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Не удалось обновить цели", "targetsErrorUpdateDescription": "Произошла ошибка при обновлении целей", "targetTlsUpdate": "Настройки TLS обновлены", - "targetTlsUpdateDescription": "Ваши настройки TLS были успешно обновлены", + "targetTlsUpdateDescription": "Настройки TLS успешно обновлены", "targetErrorTlsUpdate": "Не удалось обновить настройки TLS", "targetErrorTlsUpdateDescription": "Произошла ошибка при обновлении настроек TLS", "proxyUpdated": "Настройки прокси обновлены", - "proxyUpdatedDescription": "Ваши настройки прокси были успешно обновлены", + "proxyUpdatedDescription": "Настройки прокси успешно обновлены", "proxyErrorUpdate": "Не удалось обновить настройки прокси", "proxyErrorUpdateDescription": "Произошла ошибка при обновлении настроек прокси", - "targetAddr": "IP / Имя хоста", + "targetAddr": "Хост", "targetPort": "Порт", "targetProtocol": "Протокол", "targetTlsSettings": "Конфигурация безопасного соединения", - "targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса", + "targetTlsSettingsDescription": "Настроить параметры SSL/TLS для ресурса", "targetTlsSettingsAdvanced": "Расширенные настройки TLS", "targetTlsSni": "Имя TLS сервера", "targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.", "targetTlsSubmit": "Сохранить настройки", "targets": "Конфигурация целей", - "targetsDescription": "Настройте цели для маршрутизации трафика к вашим бэкэнд сервисам", + "targetsDescription": "Настроить цели на маршрут трафика в сервисы backend", "targetStickySessions": "Включить фиксированные сессии", "targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.", "methodSelect": "Выберите метод", "targetSubmit": "Добавить цель", - "targetNoOne": "Этот ресурс не имеет никаких целей. Добавьте цель для настройки, где отправлять запросы к вашему бэкэнду.", + "targetNoOne": "Этот ресурс не имеет никаких целей. Добавьте цель для настройки, где отправлять запросы в бэкэнд.", "targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.", "targetsSubmit": "Сохранить цели", "addTarget": "Добавить цель", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Цель была успешно создана", "targetErrorCreate": "Не удалось создать цель", "targetErrorCreateDescription": "Произошла ошибка при создании цели", + "tlsServerName": "Имя TLS сервера", + "tlsServerNameDescription": "Имя TLS сервера для SNI", "save": "Сохранить", "proxyAdditional": "Дополнительные настройки прокси", - "proxyAdditionalDescription": "Настройте, как ваш ресурс обрабатывает настройки прокси", + "proxyAdditionalDescription": "Настроить обработку параметров прокси ресурса", "proxyCustomHeader": "Пользовательский заголовок Host", "proxyCustomHeaderDescription": "Заголовок host для установки при проксировании запросов. Оставьте пустым для использования по умолчанию.", "proxyAdditionalSubmit": "Сохранить настройки прокси", @@ -558,7 +572,7 @@ "rulesMatchType": "Тип совпадения", "value": "Значение", "rulesAbout": "О правилах", - "rulesAboutDescription": "Правила позволяют контролировать доступ к вашему ресурсу на основе набора критериев. Вы можете создавать правила для разрешения или запрета доступа на основе IP адреса или URL пути.", + "rulesAboutDescription": "Правила позволяют контролировать доступ к ресурсу на основе набора критериев. Вы можете создать правила, чтобы разрешить или запретить доступ на основе IP-адреса или URL пути.", "rulesActions": "Действия", "rulesActionAlwaysAllow": "Всегда разрешать: Обойти все методы аутентификации", "rulesActionAlwaysDeny": "Всегда запрещать: Блокировать все запросы; аутентификация не может быть выполнена", @@ -570,7 +584,7 @@ "rulesEnable": "Включить правила", "rulesEnableDescription": "Включить или отключить проверку правил для этого ресурса", "rulesResource": "Конфигурация правил ресурса", - "rulesResourceDescription": "Настройте правила для контроля доступа к вашему ресурсу", + "rulesResourceDescription": "Настройка правил для контроля доступа к ресурсу", "ruleSubmit": "Добавить правило", "rulesNoOne": "Нет правил. Добавьте правило с помощью формы.", "rulesOrder": "Правила оцениваются по приоритету в возрастающем порядке.", @@ -586,7 +600,7 @@ "none": "Нет", "unknown": "Неизвестно", "resources": "Ресурсы", - "resourcesDescription": "Ресурсы - это прокси к приложениям, работающим в вашей частной сети. Создайте ресурс для любого HTTP/HTTPS или сырого TCP/UDP сервиса в вашей частной сети. Каждый ресурс должен быть подключен к сайту для обеспечения приватного, безопасного соединения через зашифрованный туннель WireGuard.", + "resourcesDescription": "Ресурсы - это прокси для приложений, работающих в частной сети. Создайте ресурс для любого HTTP/HTTPS или необработанной службы TCP/UDP в вашей частной сети. Каждый ресурс должен быть подключен к сайту, чтобы включить приватное и защищенное подключение через зашифрованный туннель WireGuard.", "resourcesWireGuardConnect": "Безопасное соединение с шифрованием WireGuard", "resourcesMultipleAuthenticationMethods": "Настройка нескольких методов аутентификации", "resourcesUsersRolesAccess": "Контроль доступа на основе пользователей и ролей", @@ -597,7 +611,7 @@ "resourceSelect": "Выберите ресурс", "shareLinks": "Общие ссылки", "share": "Общие ссылки", - "shareDescription2": "Создавайте общие ссылки к вашим ресурсам. Ссылки предоставляют временный или неограниченный доступ к вашему ресурсу. Вы можете настроить время истечения ссылки при её создании.", + "shareDescription2": "Создавайте общие ссылки на ресурсы. Ссылки обеспечивают временный или неограниченный доступ к вашему ресурсу. Вы можете настроить продолжительность действия ссылки при ее создании.", "shareEasyCreate": "Легко создавать и делиться", "shareConfigurableExpirationDuration": "Настраиваемая продолжительность истечения", "shareSecureAndRevocable": "Безопасные и отзываемые", @@ -607,19 +621,19 @@ "unknownCommand": "Неизвестная команда", "newtErrorFetchReleases": "Не удалось получить информацию о релизе: {err}", "newtErrorFetchLatest": "Ошибка при получении последнего релиза: {err}", - "newtEndpoint": "Конечная точка Newt", - "newtId": "Newt ID", - "newtSecretKey": "Секретный ключ Newt", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "Секретный ключ", "architecture": "Архитектура", "sites": "Сайты", - "siteWgAnyClients": "Используйте любой клиент WireGuard для подключения. Вам придётся обращаться к вашим внутренним ресурсам, используя IP узла.", + "siteWgAnyClients": "Для подключения используйте любой клиент WireGuard. Вы должны будете адресовать внутренние ресурсы, используя IP адрес пира.", "siteWgCompatibleAllClients": "Совместим со всеми клиентами WireGuard", "siteWgManualConfigurationRequired": "Требуется ручная настройка", "userErrorNotAdminOrOwner": "Пользователь не является администратором или владельцем", "pangolinSettings": "Настройки - Pangolin", "accessRoleYour": "Ваша роль:", - "accessRoleSelect2": "Выберите роль", - "accessUserSelect": "Выберите пользователя", + "accessRoleSelect2": "Выберите роли", + "accessUserSelect": "Выберите пользователей", "otpEmailEnter": "Введите email", "otpEmailEnterDescription": "Нажмите enter для добавления email после ввода в поле.", "otpEmailErrorInvalid": "Неверный email адрес. Подстановочный знак (*) должен быть всей локальной частью.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Установить PIN-код", "resourcePincodeSetupTitleDescription": "Установите PIN-код для защиты этого ресурса", "resourceRoleDescription": "Администраторы всегда имеют доступ к этому ресурсу.", - "resourceUsersRoles": "Пользователи и роли", + "resourceUsersRoles": "Контроль доступа", "resourceUsersRolesDescription": "Выберите пользователей и роли с доступом к этому ресурсу", "resourceUsersRolesSubmit": "Сохранить пользователей и роли", "resourceWhitelistSave": "Успешно сохранено", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Перенести ресурс", "siteDestination": "Новый сайт для ресурса", "searchSites": "Поиск сайтов", + "countries": "Страны", "accessRoleCreate": "Создание роли", "accessRoleCreateDescription": "Создайте новую роль для группы пользователей и выдавайте им разрешения.", "accessRoleCreateSubmit": "Создать роль", @@ -766,15 +781,15 @@ "idpOidcConfigure": "Конфигурация OAuth2/OIDC", "idpOidcConfigureDescription": "Настройте конечные точки и учётные данные поставщика OAuth2/OIDC", "idpClientId": "ID клиента", - "idpClientIdDescription": "OAuth2 ID клиента от вашего поставщика удостоверений", + "idpClientIdDescription": "Идентификатор клиента OAuth2 от поставщика идентификации", "idpClientSecret": "Секрет клиента", - "idpClientSecretDescription": "OAuth2 секрет клиента от вашего поставщика удостоверений", + "idpClientSecretDescription": "Секретный ключ клиента OAuth2 от поставщика идентификации", "idpAuthUrl": "URL авторизации", "idpAuthUrlDescription": "URL конечной точки авторизации OAuth2", "idpTokenUrl": "URL токена", "idpTokenUrlDescription": "URL конечной точки токена OAuth2", "idpOidcConfigureAlert": "Важная информация", - "idpOidcConfigureAlertDescription": "После создания поставщика удостоверений вам нужно будет настроить URL обратного вызова в настройках вашего поставщика удостоверений. URL обратного вызова будет предоставлен после успешного создания.", + "idpOidcConfigureAlertDescription": "После создания идентификационного провайдера вам необходимо настроить обратный адрес в настройках провайдера. URL обратного вызова будет предоставлен после успешного создания.", "idpToken": "Конфигурация токена", "idpTokenDescription": "Настройте, как извлекать информацию о пользователе из ID токена", "idpJmespathAbout": "О JMESPath", @@ -791,7 +806,7 @@ "idpSubmit": "Создать поставщика удостоверений", "orgPolicies": "Политики организации", "idpSettings": "Настройки {idpName}", - "idpCreateSettingsDescription": "Настройте параметры для вашего поставщика удостоверений", + "idpCreateSettingsDescription": "Настройка параметров для идентификации провайдера", "roleMapping": "Сопоставление ролей", "orgMapping": "Сопоставление организаций", "orgPoliciesSearch": "Поиск политик организации...", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "Поставщик удостоверений успешно обновлён", "redirectUrl": "URL редиректа", "redirectUrlAbout": "О редиректе URL", - "redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках вашего поставщика удостоверений.", + "redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках провайдера.", "pangolinAuth": "Аутентификация - Pangolin", "verificationCodeLengthRequirements": "Ваш код подтверждения должен состоять из 8 символов.", "errorOccurred": "Произошла ошибка", @@ -909,6 +924,10 @@ "passwordResetSent": "Мы отправим код сброса пароля на этот email адрес.", "passwordResetCode": "Код сброса пароля", "passwordResetCodeDescription": "Проверьте вашу почту для получения кода сброса пароля.", + "generatePasswordResetCode": "Сгенерировать код сброса пароля", + "passwordResetCodeGenerated": "Код сброса пароля создан", + "passwordResetCodeGeneratedDescription": "Поделитесь этим кодом с пользователем. Они могут использовать его для сброса пароля.", + "passwordResetUrl": "Reset URL", "passwordNew": "Новый пароль", "passwordNewConfirm": "Подтвердите новый пароль", "changePassword": "Изменить пароль", @@ -926,6 +945,9 @@ "pincodeAuth": "Код аутентификатора", "pincodeSubmit2": "Отправить код", "passwordResetSubmit": "Запросить сброс", + "passwordResetAlreadyHaveCode": "Введите код сброса пароля", + "passwordResetSmtpRequired": "Пожалуйста, обратитесь к администратору", + "passwordResetSmtpRequiredDescription": "Для сброса пароля необходим код сброса пароля. Обратитесь к администратору за помощью.", "passwordBack": "Назад к паролю", "loginBack": "Вернуться к входу", "signup": "Регистрация", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Список ресурсов сайта", "actionUpdateSiteResource": "Обновить ресурс сайта", "actionListInvitations": "Список приглашений", + "actionExportLogs": "Экспорт журналов", + "actionViewLogs": "Просмотр журналов", "noneSelected": "Ничего не выбрано", "orgNotFound2": "Организации не найдены.", "searchProgress": "Поиск...", "create": "Создать", "orgs": "Организации", "loginError": "Произошла ошибка при входе", + "loginRequiredForDevice": "Для аутентификации устройства необходимо войти в систему.", "passwordForgot": "Забыли пароль?", "otpAuth": "Двухфакторная аутентификация", "otpAuthDescription": "Введите код из вашего приложения-аутентификатора или один из ваших одноразовых резервных кодов.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Главная", "sidebarSites": "Сайты", "sidebarResources": "Ресурсы", + "sidebarProxyResources": "Публичный", + "sidebarClientResources": "Приватный", "sidebarAccessControl": "Контроль доступа", + "sidebarLogsAndAnalytics": "Журналы и аналитика", "sidebarUsers": "Пользователи", + "sidebarAdmin": "Админ", "sidebarInvitations": "Приглашения", "sidebarRoles": "Роли", - "sidebarShareableLinks": "Общие ссылки", + "sidebarShareableLinks": "Ссылки", "sidebarApiKeys": "API ключи", "sidebarSettings": "Настройки", "sidebarAllUsers": "Все пользователи", "sidebarIdentityProviders": "Поставщики удостоверений", "sidebarLicense": "Лицензия", "sidebarClients": "Клиенты", + "sidebarUserDevices": "Пользователи", + "sidebarMachineClients": "Машины", "sidebarDomains": "Домены", + "sidebarGeneral": "Общие", + "sidebarLogAndAnalytics": "Журнал и аналитика", "sidebarBluePrints": "Чертежи", + "sidebarOrganization": "Организация", + "sidebarLogsAnalytics": "Статистика", "blueprints": "Чертежи", "blueprintsDescription": "Применить декларирующие конфигурации и просмотреть предыдущие запуски", "blueprintAdd": "Добавить чертёж", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Посмотреть результат примененного чертежа и все возникшие ошибки", "blueprintInfo": "Информация о чертеже", "message": "Сообщение", - "blueprintContentsDescription": "Определите содержимое YAML, описывающее вашу инфраструктуру", + "blueprintContentsDescription": "Определите содержимое YAML, описывающее инфраструктуру", "blueprintErrorCreateDescription": "Произошла ошибка при применении чертежа", "blueprintErrorCreate": "Ошибка при создании чертежа", "searchBlueprintProgress": "Поиск чертежей...", @@ -1230,15 +1265,15 @@ "loading": "Загрузка", "restart": "Перезагрузка", "domains": "Домены", - "domainsDescription": "Управление доменами для вашей организации", + "domainsDescription": "Создание и управление доменами, доступными в организации", "domainsSearch": "Поиск доменов...", "domainAdd": "Добавить Домен", - "domainAddDescription": "Зарегистрировать новый домен в вашей организации", + "domainAddDescription": "Зарегистрировать новый домен в организации", "domainCreate": "Создать Домен", "domainCreatedDescription": "Домен успешно создан", "domainDeletedDescription": "Домен успешно удален", - "domainQuestionRemove": "Вы уверены, что хотите удалить домен из вашей учетной записи?", - "domainMessageRemove": "После удаления домен больше не будет связан с вашей учетной записью.", + "domainQuestionRemove": "Вы уверены, что хотите удалить домен?", + "domainMessageRemove": "После удаления домен больше не будет связан с организацией.", "domainConfirmDelete": "Подтвердить удаление домена", "domainDelete": "Удалить Домен", "domain": "Домен", @@ -1257,7 +1292,7 @@ "pending": "В ожидании", "sidebarBilling": "Выставление счетов", "billing": "Выставление счетов", - "orgBillingDescription": "Управляйте информацией о выставлении счетов и подписками", + "orgBillingDescription": "Управление платежной информацией и подписками", "github": "GitHub", "pangolinHosted": "Pangolin Hosted", "fossorial": "Fossorial", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Обновления продуктов", "productUpdateEmpty": "Нет обновлений", "dismissAll": "Отклонить все", - "pangolinUpdateAvailable": "Доступна новая версия", + "pangolinUpdateAvailable": "Доступно обновление", "pangolinUpdateAvailableInfo": "Версия {version} готова к установке", - "pangolinUpdateAvailableReleaseNotes": "Просмотреть заметки о выпуске", + "pangolinUpdateAvailableReleaseNotes": "Просмотреть примечания к выпуску", "newtUpdateAvailable": "Доступно обновление", "newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.", "domainPickerEnterDomain": "Домен", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "А-Я", "domainPickerSortDesc": "Я-А", "domainPickerCheckingAvailability": "Проверка доступности...", - "domainPickerNoMatchingDomains": "Не найдены сопоставимые домены. Попробуйте другой домен или проверьте настройки доменов вашей организации.", + "domainPickerNoMatchingDomains": "Подходящие домены не найдены. Попробуйте другой домен или проверьте настройки домена организации.", "domainPickerOrganizationDomains": "Домены организации", "domainPickerProvidedDomains": "Предоставленные домены", "domainPickerSubdomain": "Поддомен: {subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "Изменить подписку", "billingStartSubscription": "Начать подписку", "billingRecurringCharge": "Периодический взнос", - "billingManageSubscriptionSettings": "Управляйте настройками и предпочтениями вашей подписки", + "billingManageSubscriptionSettings": "Управление настройками и настройками подписки", "billingNoActiveSubscription": "У вас нет активной подписки. Начните подписку, чтобы увеличить лимиты использования.", "billingFailedToLoadSubscription": "Не удалось загрузить подписку", "billingFailedToLoadUsage": "Не удалось загрузить использование", @@ -1345,9 +1380,9 @@ "billingPortalError": "Ошибка портала", "billingDataUsageInfo": "Вы несете ответственность за все данные, переданные через безопасные туннели при подключении к облаку. Это включает как входящий, так и исходящий трафик на всех ваших сайтах. При достижении лимита ваши сайты будут отключаться до тех пор, пока вы не обновите план или не уменьшите его использование. При использовании узлов не взимается плата.", "billingOnlineTimeInfo": "Вы тарифицируете на то, как долго ваши сайты будут подключены к облаку. Например, 44 640 минут равны одному сайту, работающему круглосуточно за весь месяц. Когда вы достигните лимита, ваши сайты будут отключаться до тех пор, пока вы не обновите тарифный план или не сократите нагрузку. При использовании узлов не тарифицируется.", - "billingUsersInfo": "С вас взимается плата за каждого пользователя в вашей организации. Оплата рассчитывается ежедневно исходя из количества активных учетных записей пользователей в вашей организации.", - "billingDomainInfo": "С вас взимается плата за каждый домен в вашей организации. Оплата рассчитывается ежедневно исходя из количества активных учетных записей доменов в вашей организации.", - "billingRemoteExitNodesInfo": "С вас взимается плата за каждый управляемый узел в вашей организации. Оплата рассчитывается ежедневно исходя из количества активных управляемых узлов в вашей организации.", + "billingUsersInfo": "Вы оплачиваете за каждого пользователя в организации. Платеж рассчитывается ежедневно в зависимости от количества активных учетных записей в вашем органе.", + "billingDomainInfo": "Вы платите за каждый домен в организации. Платеж рассчитывается ежедневно в зависимости от количества активных доменных аккаунтов в вашем органе.", + "billingRemoteExitNodesInfo": "Вы платите за каждый управляемый узел организации. Платёж рассчитывается ежедневно на основе количества активных управляемых узлов в вашем органе.", "domainNotFound": "Домен не найден", "domainNotFoundDescription": "Этот ресурс отключен, так как домен больше не существует в нашей системе. Пожалуйста, установите новый домен для этого ресурса.", "failed": "Ошибка", @@ -1430,29 +1465,32 @@ "and": "и", "privacyPolicy": "политика конфиденциальности" }, + "signUpMarketing": { + "keepMeInTheLoop": "Держите меня в цикле с новостями, обновлениями и новыми функциями по электронной почте." + }, "siteRequired": "Необходимо указать сайт.", "olmTunnel": "Olm Туннель", "olmTunnelDescription": "Используйте Olm для подключений клиентов", "errorCreatingClient": "Ошибка при создании клиента", "clientDefaultsNotFound": "Настройки клиента по умолчанию не найдены", "createClient": "Создать клиента", - "createClientDescription": "Создайте нового клиента для подключения к вашим сайтам", + "createClientDescription": "Создайте нового клиента для доступа к приватным ресурсам", "seeAllClients": "Просмотреть всех клиентов", "clientInformation": "Информация о клиенте", "clientNamePlaceholder": "Имя клиента", "address": "Адрес", "subnetPlaceholder": "Подсеть", - "addressDescription": "Адрес, который этот клиент будет использовать для подключения", + "addressDescription": "Внутренний адрес клиента. Должен находиться в подсети организации.", "selectSites": "Выберите сайты", "sitesDescription": "Клиент будет иметь подключение к выбранным сайтам", "clientInstallOlm": "Установить Olm", "clientInstallOlmDescription": "Запустите Olm на вашей системе", - "clientOlmCredentials": "Учётные данные Olm", - "clientOlmCredentialsDescription": "Так Olm будет аутентифицироваться через сервер", - "olmEndpoint": "Конечная точка Olm", - "olmId": "Olm ID", - "olmSecretKey": "Секретный ключ Olm", - "clientCredentialsSave": "Сохраните ваши учётные данные", + "clientOlmCredentials": "Полномочия", + "clientOlmCredentialsDescription": "Именно так клиент будет аутентифицироваться с сервером", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "Секретный ключ", + "clientCredentialsSave": "Сохранить учетные данные", "clientCredentialsSaveDescription": "Вы сможете увидеть их только один раз. Обязательно скопируйте в безопасное место.", "generalSettingsDescription": "Настройте общие параметры для этого клиента", "clientUpdated": "Клиент обновлен", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Произошла ошибка при получении сайтов.", "olmErrorFetchReleases": "Произошла ошибка при получении релизов Olm.", "olmErrorFetchLatest": "Произошла ошибка при получении последнего релиза Olm.", - "remoteSubnets": "Удалённые подсети", "enterCidrRange": "Введите диапазон CIDR", - "remoteSubnetsDescription": "Добавьте диапазоны адресов CIDR, которые можно получить из этого сайта удаленно, используя клиентов. Используйте формат 10.0.0.0/24. Это относится ТОЛЬКО к подключению через VPN клиентов.", "resourceEnableProxy": "Включить публичный прокси", "resourceEnableProxyDescription": "Включите публичное проксирование для этого ресурса. Это позволяет получить доступ к ресурсу извне сети через облако через открытый порт. Требуется конфигурация Traefik.", "externalProxyEnabled": "Внешний прокси включен", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Мониторинг здоровья этой цели. При необходимости можно контролировать другую конечную точку.", "healthScheme": "Метод", "healthSelectScheme": "Выберите метод", + "healthCheckPortInvalid": "Порт проверки здоровья должен быть от 1 до 65535", "healthCheckPath": "Путь", "healthHostname": "IP / хост", "healthPort": "Порт", "healthCheckPathDescription": "Путь к проверке состояния здоровья.", - "healthyIntervalSeconds": "Интервал здоровых состояний", - "unhealthyIntervalSeconds": "Интервал нездоровых состояний", + "healthyIntervalSeconds": "Здоровой Интервал (сек)", + "unhealthyIntervalSeconds": "Нездоровый интервал (сек)", "IntervalSeconds": "Интервал здоровых состояний", - "timeoutSeconds": "Тайм-аут", + "timeoutSeconds": "Таймаут (сек)", "timeIsInSeconds": "Время указано в секундах", "retryAttempts": "Количество попыток повторного запроса", "expectedResponseCodes": "Ожидаемые коды ответов", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Редактировать домен", "siteName": "Имя сайта", "proxyPort": "Порт", - "resourcesTableProxyResources": "Проксированные ресурсы", - "resourcesTableClientResources": "Клиентские ресурсы", + "resourcesTableProxyResources": "Публичный", + "resourcesTableClientResources": "Приватный", "resourcesTableNoProxyResourcesFound": "Проксированных ресурсов не найдено.", "resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.", "resourcesTableDestination": "Пункт назначения", - "resourcesTableTheseResourcesForUseWith": "Эти ресурсы предназначены для использования с", + "resourcesTableAlias": "Alias", "resourcesTableClients": "Клиенты", "resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.", "resourcesTableNoTargets": "Нет ярлыков", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Оффлайн", "resourcesTableUnknown": "Неизвестен", "resourcesTableNotMonitored": "Не отслеживается", - "editInternalResourceDialogEditClientResource": "Редактировать ресурс клиента", - "editInternalResourceDialogUpdateResourceProperties": "Обновите свойства ресурса и настройку цели для {resourceName}.", + "editInternalResourceDialogEditClientResource": "Изменить приватный ресурс", + "editInternalResourceDialogUpdateResourceProperties": "Обновить настройки ресурса и элементы управления доступом для {resourceName}", "editInternalResourceDialogResourceProperties": "Свойства ресурса", "editInternalResourceDialogName": "Имя", "editInternalResourceDialogProtocol": "Протокол", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "Неверный формат IP адреса", "editInternalResourceDialogDestinationPortMin": "Целевой порт должен быть не менее 1", "editInternalResourceDialogDestinationPortMax": "Целевой порт должен быть меньше 65536", + "editInternalResourceDialogPortModeRequired": "Порт для порта необходим для протокола, прокси и порта назначения", + "editInternalResourceDialogMode": "Режим", + "editInternalResourceDialogModePort": "Порт", + "editInternalResourceDialogModeHost": "Хост", + "editInternalResourceDialogModeCidr": "СИДР", + "editInternalResourceDialogDestination": "Пункт назначения", + "editInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.", + "editInternalResourceDialogDestinationIPDescription": "IP или адрес хоста ресурса в сети сайта.", + "editInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.", "createInternalResourceDialogNoSitesAvailable": "Нет доступных сайтов", "createInternalResourceDialogNoSitesAvailableDescription": "Вам необходимо иметь хотя бы один сайт Newt с настроенной подсетью для создания внутреннего ресурса.", "createInternalResourceDialogClose": "Закрыть", - "createInternalResourceDialogCreateClientResource": "Создать ресурс клиента", - "createInternalResourceDialogCreateClientResourceDescription": "Создайте новый ресурс, который будет доступен клиентам, подключенным к выбранному сайту.", + "createInternalResourceDialogCreateClientResource": "Создать приватный ресурс", + "createInternalResourceDialogCreateClientResourceDescription": "Создать новый ресурс, который будет доступен только клиентам, подключенным к организации", "createInternalResourceDialogResourceProperties": "Свойства ресурса", "createInternalResourceDialogName": "Имя", "createInternalResourceDialogSite": "Сайт", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "Неверный формат IP-адреса", "createInternalResourceDialogDestinationPortMin": "Целевой порт должен быть не менее 1", "createInternalResourceDialogDestinationPortMax": "Целевой порт должен быть меньше 65536", + "createInternalResourceDialogPortModeRequired": "Порт для порта необходим для протокола, прокси и порта назначения", + "createInternalResourceDialogMode": "Режим", + "createInternalResourceDialogModePort": "Порт", + "createInternalResourceDialogModeHost": "Хост", + "createInternalResourceDialogModeCidr": "СИДР", + "createInternalResourceDialogDestination": "Пункт назначения", + "createInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.", + "createInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.", "siteConfiguration": "Конфигурация", "siteAcceptClientConnections": "Принимать подключения клиентов", - "siteAcceptClientConnectionsDescription": "Разрешите другим устройствам подключаться через этот экземпляр Newt в качестве шлюза с использованием клиентов.", - "siteAddress": "Адрес сайта", - "siteAddressDescription": "Укажите IP-адрес хоста для подключения клиентов. Это внутренний адрес сайта в сети Pangolin для адресации клиентов. Должен находиться в пределах подсети организационного уровня.", + "siteAcceptClientConnectionsDescription": "Разрешить пользовательским устройствам и клиентам доступ к ресурсам на этом сайте. Это может быть изменено позже.", + "siteAddress": "Адрес сайта (Дополнительно)", + "siteAddressDescription": "Внутренний адрес сайта. Должен находиться в подсети организации.", + "siteNameDescription": "Отображаемое имя сайта, которое может быть изменено позже.", "autoLoginExternalIdp": "Автоматический вход с внешним провайдером", "autoLoginExternalIdpDescription": "Немедленно перенаправьте пользователя к внешнему провайдеру для аутентификации.", "selectIdp": "Выберите провайдера", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.", "autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.", "remoteExitNodeManageRemoteExitNodes": "Удаленные узлы", - "remoteExitNodeDescription": "Самохост-один или несколько удаленных узлов для расширения сетевого подключения и уменьшения зависимости от облака", + "remoteExitNodeDescription": "Самохост-один или несколько удаленных узлов для расширения сетевого соединения и уменьшения зависимости от облака", "remoteExitNodes": "Узлы", "searchRemoteExitNodes": "Поиск узлов...", "remoteExitNodeAdd": "Добавить узел", @@ -1623,11 +1682,11 @@ "sidebarRemoteExitNodes": "Удаленные узлы", "remoteExitNodeCreate": { "title": "Создать узел", - "description": "Создайте новый узел, чтобы расширить сетевое подключение", + "description": "Создать новый узел для расширения сетевого подключения", "viewAllButton": "Все узлы", "strategy": { "title": "Стратегия создания", - "description": "Выберите эту опцию для настройки вашего узла или создания новых учетных данных.", + "description": "Выберите эту опцию для настройки узла или создания новых учетных данных.", "adopt": { "title": "Принять узел", "description": "Выберите это, если у вас уже есть учетные данные для узла." @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Сгенерированные учетные данные", - "description": "Используйте эти учётные данные для настройки вашего узла", + "description": "Используйте эти учётные данные для настройки узла", "nodeIdTitle": "ID узла", "secretTitle": "Секретный ключ", "saveCredentialsTitle": "Добавить учетные данные в конфигурацию", @@ -1725,14 +1784,14 @@ "roleMappingExpressionPlaceholder": "например, contains(groups, 'admin') && 'Admin' || 'Member'", "idpGoogleConfiguration": "Конфигурация Google", "idpGoogleConfigurationDescription": "Настройка учетных данных Google OAuth2", - "idpGoogleClientIdDescription": "Ваш Google OAuth2 ID клиента", - "idpGoogleClientSecretDescription": "Ваш Google OAuth2 Секрет", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Секрет клиента Google OAuth2", "idpAzureConfiguration": "Конфигурация Azure Entra ID", - "idpAzureConfigurationDescription": "Настройте учетные данные Azure Entra ID OAuth2", + "idpAzureConfigurationDescription": "Настройка учетных данных Azure Entra ID OAuth2", "idpTenantId": "Идентификатор арендатора", - "idpTenantIdPlaceholder": "ваш тенант-id", - "idpAzureTenantIdDescription": "Идентификатор арендатора Azure (найден в обзоре Active Directory Azure)", - "idpAzureClientIdDescription": "Ваш идентификатор клиента Azure App", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "ID арендатора Azure (найден в обзоре Active Directory Azure)", + "idpAzureClientIdDescription": "Регистрационный номер клиента Azure App", "idpAzureClientSecretDescription": "Секрет регистрации клиента Azure App", "idpGoogleTitle": "Google", "idpGoogleAlt": "Google", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Конфигурация Google", "idpAzureConfigurationTitle": "Конфигурация Azure Entra ID", "idpTenantIdLabel": "Идентификатор арендатора", - "idpAzureClientIdDescription2": "Ваш идентификатор клиента Azure App", + "idpAzureClientIdDescription2": "Регистрационный номер клиента Azure App", "idpAzureClientSecretDescription2": "Секрет регистрации клиента Azure App", "idpGoogleDescription": "Google OAuth2/OIDC провайдер", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Подсеть", "subnetDescription": "Подсеть для конфигурации сети этой организации.", "authPage": "Страница авторизации", - "authPageDescription": "Настройка страницы авторизации для вашей организации", + "authPageDescription": "Настроить страницу авторизации для организации", "authPageDomain": "Домен страницы авторизации", "noDomainSet": "Домен не установлен", "changeDomain": "Изменить домен", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "Установить домен страницы авторизации", "failedToFetchCertificate": "Не удалось получить сертификат", "failedToRestartCertificate": "Не удалось перезапустить сертификат", - "addDomainToEnableCustomAuthPages": "Добавьте домен для включения пользовательских страниц аутентификации для вашей организации", + "addDomainToEnableCustomAuthPages": "Добавить домен для включения пользовательских страниц аутентификации для организации", "selectDomainForOrgAuthPage": "Выберите домен для страницы аутентификации организации", "domainPickerProvidedDomain": "Домен предоставлен", "domainPickerFreeProvidedDomain": "Бесплатный домен", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" не может быть действительным для {domain}.", "domainPickerSubdomainSanitized": "Субдомен очищен", "domainPickerSubdomainCorrected": "\"{sub}\" был исправлен на \"{sanitized}\"", - "orgAuthSignInTitle": "Войдите в свою организацию", + "orgAuthSignInTitle": "Войти в организацию", "orgAuthChooseIdpDescription": "Выберите своего поставщика удостоверений личности для продолжения", "orgAuthNoIdpConfigured": "Эта организация не имеет настроенных поставщиков идентификационных данных. Вместо этого вы можете войти в свой Pangolin.", "orgAuthSignInWithPangolin": "Войти через Pangolin", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "Включить двухфакторную аутентификацию", "completeSecuritySteps": "Пройти шаги безопасности", "securitySettings": "Настройки безопасности", - "securitySettingsDescription": "Настройка политик безопасности для вашей организации", + "securitySettingsDescription": "Настройка политик безопасности для организации", "requireTwoFactorForAllUsers": "Требовать двухфакторную аутентификацию для всех пользователей", "requireTwoFactorDescription": "Когда включено, все внутренние пользователи в этой организации должны иметь двухфакторную аутентификацию для доступа к организации.", "requireTwoFactorDisabledDescription": "Эта функция требует действительной лицензии (Enterprise) или активной подписки (SaaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Корпоративная версия", "unlicensed": "Нелицензированный", "beta": "Бета", - "manageClients": "Управление клиентами", - "manageClientsDescription": "Клиенты - это устройства, которые могут подключаться к вашим сайтам", + "manageUserDevices": "Устройства пользователя", + "manageUserDevicesDescription": "Просмотр и управление устройствами, которые пользователи используют для приватного подключения к ресурсам", + "manageMachineClients": "Управление машинными клиентами", + "manageMachineClientsDescription": "Создание и управление клиентами, которые используют серверы и системы для частного подключения к ресурсам", + "clientsTableUserClients": "Пользователь", + "clientsTableMachineClients": "Машина", "licenseTableValidUntil": "Действителен до", "saasLicenseKeysSettingsTitle": "Корпоративные лицензии", "saasLicenseKeysSettingsDescription": "Генерировать и управлять лицензионными ключами Enterprise для копий Pangolin", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "полоса", "sidebarEnableEnterpriseLicense": "Включить корпоративную лицензию", "cannotbeUndone": "Это действие не может быть отменено.", - "toConfirm": "для подтверждения", + "toConfirm": "для подтверждения.", "deleteClientQuestion": "Вы уверены, что хотите удалить клиента из сайта и организации?", "clientMessageRemove": "После удаления клиент больше не сможет подключиться к сайту.", "sidebarLogs": "Логи", "request": "Запросить", + "requests": "Запросы", "logs": "Логи", "logsSettingsDescription": "Отслеживать журналы, собранные в этой организации", "searchLogs": "Поиск журналов...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Причина", "requestLogs": "Запросить журналы", + "requestAnalytics": "Аналитика запроса", "host": "Хост", "location": "Местоположение", "actionLogs": "Журнал действий", @@ -2029,6 +2094,7 @@ "logRetention": "Сохранение журнала", "logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их", "requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации", + "requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации", "logRetentionRequestLabel": "Запросить сохранение журнала", "logRetentionRequestDescription": "Как долго сохранять журналы запросов", "logRetentionAccessLabel": "Хранение журнала доступа", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 дней", "logRetention90Days": "90 дней", "logRetentionForever": "Всегда", + "logRetentionEndOfFollowingYear": "Конец следующего года", "actionLogsDescription": "Просмотр истории действий, выполненных в этой организации", "accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации", "licenseRequiredToUse": "Для использования этой функции требуется лицензия предприятия.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Предпочитать сертификат Wildcard", "unverified": "Не подтверждено", "domainSetting": "Настройки домена", - "domainSettingDescription": "Настройка параметров для вашего домена", + "domainSettingDescription": "Настройка параметров домена", "preferWildcardCertDescription": "Попытка создания шаблона сертификата (требуется должным образом сконфигурированный резолвер сертификата).", "recordName": "Имя записи", "auto": "Авто", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "Доступна обновленная версия Олма. Пожалуйста, обновитесь до последней версии.", "client": "Клиент", "proxyProtocol": "Настройки протокола прокси", - "proxyProtocolDescription": "Настроить Прокси-протокол для сохранения IP-адресов клиента для служб TCP/UDP.", + "proxyProtocolDescription": "Настроить Прокси-протокол для сохранения IP-адресов клиента для служб TCP.", "enableProxyProtocol": "Включить Прокси Протокол", - "proxyProtocolInfo": "Сохранять IP-адреса клиента для кэша TCP/UDP", + "proxyProtocolInfo": "Сохранять IP-адреса клиента для backend'ов TCP", "proxyProtocolVersion": "Версия протокола прокси", "version1": " Версия 1 (рекомендуется)", "version2": "Версия 2", "versionDescription": "Версия 1 основана на тексте и широко поддерживается. Версия 2 является бинарной и более эффективной, но менее совместимой.", "warning": "Предупреждение", - "proxyProtocolWarning": "Бэкэнд приложение должно быть сконфигурировано для принятия прокси-соединений. Если ваш бэкэнд не поддерживает Прокси-протокол, это нарушит все соединения. Обязательно настройте вашего бэкэнда на доверие заголовкам Proxy Protocol от Traefik.", + "proxyProtocolWarning": "Бэкэнд приложение должно быть настроено на принятие соединений прокси-протокола. Если ваш бэкэнд не поддерживает Прокси-протокол, то включение этой опции прервет все подключения, поэтому включите это только если вы знаете, что вы делаете. Обязательно настройте вашего бэкэнда на доверие заголовкам Proxy Protocol от Traefik.", "restarting": "Перезапуск...", "manual": "Ручной", "messageSupport": "Поддержка сообщений", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Сообщение отправлено!", "supportWillContact": "Мы скоро свяжемся с Вами!", "selectLogRetention": "Выберите удержание журнала", + "terms": "Условия", + "privacy": "Приватность", + "security": "Безопасность", + "docs": "Документ", + "deviceActivation": "Активация устройства", + "deviceCodeInvalidFormat": "Код должен быть 9 символов (например, A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Неверный или просроченный код", + "deviceCodeVerifyFailed": "Не удалось проверить код устройства", + "signedInAs": "Вы вошли как", + "deviceCodeEnterPrompt": "Введите код, отображаемый на устройстве", + "continue": "Продолжить", + "deviceUnknownLocation": "Неизвестное местоположение", + "deviceAuthorizationRequested": "Эта авторизация была запрошена у {location} на {date}. Убедитесь, что вы доверяете этому устройству, так как оно получит доступ к учетной записи.", + "deviceLabel": "Устройство: {deviceName}", + "deviceWantsAccess": "хочет получить доступ к вашей учетной записи", + "deviceExistingAccess": "Существующий доступ:", + "deviceFullAccess": "Полный доступ к вашему аккаунту", + "deviceOrganizationsAccess": "Доступ ко всем организациям, к которым ваш аккаунт имеет доступ", + "deviceAuthorize": "Авторизовать {applicationName}", + "deviceConnected": "Устройство подключено!", + "deviceAuthorizedMessage": "Устройство авторизовано для доступа к вашей учетной записи.", + "pangolinCloud": "Облако Панголина", + "viewDevices": "Просмотр устройств", + "viewDevicesDescription": "Управление подключенными устройствами", + "noDevices": "Устройств не найдено", + "dateCreated": "Дата создания", + "unnamedDevice": "Безымянное устройство", + "deviceQuestionRemove": "Вы уверены, что хотите удалить это устройство?", + "deviceMessageRemove": "Это действие нельзя отменить.", + "deviceDeleteConfirm": "Удалить устройство", + "deleteDevice": "Удалить устройство", + "errorLoadingDevices": "Ошибка загрузки устройств", + "failedToLoadDevices": "Не удалось загрузить устройства", + "deviceDeleted": "Устройство удалено", + "deviceDeletedDescription": "Устройство успешно удалено.", + "errorDeletingDevice": "Ошибка удаления устройства", + "failedToDeleteDevice": "Не удалось удалить устройство", "showColumns": "Показать колонки", "hideColumns": "Скрыть столбцы", "columnVisibility": "Видимость столбцов", @@ -2111,10 +2215,14 @@ "enableSelected": "Включить выбранные", "disableSelected": "Отключить выбранные", "checkSelectedStatus": "Проверить статус выбранных", + "clients": "Клиенты", + "accessClientSelect": "Выберите машинные клиенты", + "resourceClientDescription": "Машинные клиенты, которые имеют доступ к этому ресурсу", + "regenerate": "Пересоздать", "credentials": "Полномочия", "savecredentials": "Сохранить учетные данные", - "regeneratecredentials": "Пере-ключ", - "regenerateCredentials": "Сгенерировать и сохранить ваши учетные данные", + "regenerateCredentialsButton": "Пересоздать учетные данные", + "regenerateCredentials": "Пересоздать учетные данные", "generatedcredentials": "Сгенерированные учетные данные", "copyandsavethesecredentials": "Копировать и сохранить эти учетные данные", "copyandsavethesecredentialsdescription": "Эти учетные данные не будут отображаться снова после того, как вы покинете эту страницу. Сохраните их сейчас.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Учетные данные были успешно восстановлены и сохранены.", "credentialsSaveError": "Ошибка сохранения учетных данных", "credentialsSaveErrorDescription": "Произошла ошибка при восстановлении и сохранении учетных данных.", - "regenerateCredentialsWarning": "Восстановление учётных данных приведет к недействительным предыдущим. Убедитесь, что все конфигурации, использующие эти учетные данные.", + "regenerateCredentialsWarning": "Восстановление учётных данных приведет к аннулированию предыдущих учетных данных и отключению соединения. Убедитесь, что все конфигурации, использующие эти учетные данные.", "confirm": "Подтвердить", "regenerateCredentialsConfirmation": "Вы уверены, что хотите восстановить учетные данные?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "Секретный ключ", - "featureDisabledTooltip": "Эта функция доступна только в плане предприятия и требует лицензии на ее использование.", "niceId": "Неплохой ID", "niceIdUpdated": "Хороший ID обновлен", "niceIdUpdatedSuccessfully": "Неплохой ID успешно обновлен", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Произошла ошибка при обновлении Nice ID.", "niceIdCannotBeEmpty": "Неправильный ID не может быть пустым", "enterIdentifier": "Введите идентификатор", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "Не вы? Используйте другую учетную запись.", + "deviceLoginDeviceRequestingAccessToAccount": "Устройство запрашивает доступ к этой учетной записи.", + "noData": "Нет данных", + "machineClients": "Машинные клиенты", + "install": "Установить", + "run": "Запустить", + "clientNameDescription": "Отображаемое имя клиента, которое может быть изменено позже.", + "clientAddress": "Адрес клиента (Дополнительно)", + "setupFailedToFetchSubnet": "Не удалось получить подсеть по умолчанию", + "setupSubnetAdvanced": "Подсеть (Дополнительно)", + "setupSubnetDescription": "Подсеть для внутренней сети этой организации.", + "siteRegenerateAndDisconnect": "Сгенерировать и отключить", + "siteRegenerateAndDisconnectConfirmation": "Вы уверены, что хотите сгенерировать учетные данные и отключить этот сайт?", + "siteRegenerateAndDisconnectWarning": "Это позволит восстановить учетные данные и немедленно отключить сайт. Сайт будет перезапущен с новыми учетными данными.", + "siteRegenerateCredentialsConfirmation": "Вы уверены, что хотите восстановить учетные данные для этого сайта?", + "siteRegenerateCredentialsWarning": "Это позволит восстановить учетные данные. Сайт будет оставаться подключенным, пока вы не перезапустите его вручную и используйте новые учетные данные.", + "clientRegenerateAndDisconnect": "Сгенерировать и отключить", + "clientRegenerateAndDisconnectConfirmation": "Вы уверены, что хотите восстановить учетные данные и отключить этого клиента?", + "clientRegenerateAndDisconnectWarning": "Это позволит восстановить учетные данные и немедленно отключить клиент. Клиент будет перезапущен с новыми учетными данными.", + "clientRegenerateCredentialsConfirmation": "Вы уверены, что хотите сгенерировать данные для этого клиента?", + "clientRegenerateCredentialsWarning": "Это позволит восстановить учетные данные. Клиент останется подключенным, пока вы не перезапустите его вручную и воспользуетесь новыми учетными данными.", + "remoteExitNodeRegenerateAndDisconnect": "Сгенерировать и отключить", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Вы уверены, что хотите сгенерировать учетные данные и отключить этот удаленный узел?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Это позволит восстановить учётные данные и немедленно отключить удаленный узел выхода. Удаленный узел выхода должен быть перезапущен с новыми учетными данными.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Вы уверены, что хотите восстановить учетные данные для этого удаленного выхода узла?", + "remoteExitNodeRegenerateCredentialsWarning": "Это позволит восстановить учетные данные. Удалённый узел останется подключенным, пока вы не перезапустите его вручную и воспользуетесь новыми учетными данными.", + "agent": "Агент" } diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 7fc8c5ff..7119808a 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -1,12 +1,12 @@ { - "setupCreate": "Organizasyonunuzu, sitenizi ve kaynaklarınızı oluşturun", + "setupCreate": "Organizasyonu, siteyi ve kaynakları oluşturun", "setupNewOrg": "Yeni Organizasyon", "setupCreateOrg": "Organizasyon Oluştur", "setupCreateResources": "Kaynaklar Oluştur", "setupOrgName": "Organizasyon Adı", - "orgDisplayName": "Bu, organizasyonunuzun görünen adıdır.", + "orgDisplayName": "Bu organizasyonun görünen adıdır.", "orgId": "Organizasyon ID", - "setupIdentifierMessage": "Bu, organizasyonunuzun benzersiz kimliğidir. Görünen adtan ayrı olarak.", + "setupIdentifierMessage": "Bu organizasyonun benzersiz tanımlayıcısıdır.", "setupErrorIdentifier": "Organizasyon ID'si zaten alınmış. Lütfen başka bir tane seçin.", "componentsErrorNoMemberCreate": "Şu anda herhangi bir organizasyona üye değilsiniz. Başlamak için bir organizasyon oluşturun.", "componentsErrorNoMember": "Şu anda herhangi bir organizasyona üye değilsiniz.", @@ -50,7 +50,7 @@ "siteMessageRemove": "Kaldırıldıktan sonra site artık erişilebilir olmayacaktır. Siteyle ilişkilendirilmiş tüm hedefler de kaldırılacaktır.", "siteQuestionRemove": "Siteyi organizasyondan kaldırmak istediğinizden emin misiniz?", "siteManageSites": "Siteleri Yönet", - "siteDescription": "Ağınıza güvenli tüneller üzerinden bağlantı izni verin", + "siteDescription": "Özel ağlara erişimi etkinleştirmek için siteler oluşturun ve yönetin", "siteCreate": "Site Oluştur", "siteCreateDescription2": "Yeni bir site oluşturup bağlanmak için aşağıdaki adımları izleyin", "siteCreateDescription": "Kaynaklarınızı bağlamaya başlamak için yeni bir site oluşturun", @@ -89,7 +89,7 @@ "siteGeneralDescription": "Bu site için genel ayarları yapılandırın", "siteSettingDescription": "Sitenizdeki ayarları yapılandırın", "siteSetting": "{siteName} Ayarları", - "siteNewtTunnel": "Newt Tüneli (Önerilen)", + "siteNewtTunnel": "Newt Site (Önerilen)", "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.", @@ -98,8 +98,8 @@ "siteLocalDescriptionSaas": "Yerel kaynaklar yalnızca. Tünel oluşturma yok. Yalnızca uzak düğümlerde mevcuttur.", "siteSeeAll": "Tüm Siteleri Gör", "siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin", - "siteNewtCredentials": "Newt Kimlik Bilgileri", - "siteNewtCredentialsDescription": "Bu, Newt'in sunucu ile kimlik doğrulaması yapacağı yöntemdir", + "siteNewtCredentials": "Kimlik Bilgileri", + "siteNewtCredentialsDescription": "Bu, sitenin sunucu ile kimlik doğrulaması yapacağı yöntemdir", "siteCredentialsSave": "Kimlik Bilgilerinizi Kaydedin", "siteCredentialsSaveDescription": "Yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.", "siteInfo": "Site Bilgilendirmesi", @@ -144,8 +144,10 @@ "expires": "Süresi Doluyor", "never": "Asla", "shareErrorSelectResource": "Lütfen bir kaynak seçin", - "resourceTitle": "Kaynakları Yönet", - "resourceDescription": "Özel uygulamalarınıza güvenli vekil sunucular oluşturun", + "proxyResourceTitle": "Herkese Açık Kaynakları Yönet", + "proxyResourceDescription": "Bir web tarayıcısı aracılığıyla kamuya açık kaynaklar oluşturun ve yönetin", + "clientResourceTitle": "Özel Kaynakları Yönet", + "clientResourceDescription": "Sadece bağlı bir istemci aracılığıyla erişilebilen kaynakları oluşturun ve yönetin", "resourcesSearch": "Kaynakları ara...", "resourceAdd": "Kaynak Ekle", "resourceErrorDelte": "Kaynak silinirken hata", @@ -179,7 +181,7 @@ "baseDomain": "Temel Alan Adı", "subdomnainDescription": "Kaynağınızın erişilebileceği alt alan adı.", "resourceRawSettings": "TCP/UDP Ayarları", - "resourceRawSettingsDescription": "Kaynağınızın TCP/UDP üzerinden nasıl erişileceğini yapılandırın. Kaynağı, sunucudan erişebilmeniz için bir ana bilgisayar Pangolin sunucusundaki bir bağlantı noktasına eşlersiniz: sunucu genel-IP: eşlenen-bağlantı-noktası.", + "resourceRawSettingsDescription": "Kaynaklara TCP/UDP üzerinden nasıl erişileceğini yapılandırın", "protocol": "Protokol", "protocolSelect": "Bir protokol seçin", "resourcePortNumber": "Port Numarası", @@ -204,8 +206,8 @@ "rules": "Kurallar", "resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın", "resourceSetting": "{resourceName} Ayarları", - "alwaysAllow": "Her Zaman İzin Ver", - "alwaysDeny": "Her Zaman Reddet", + "alwaysAllow": "Kimlik Doğrulamayı Atla", + "alwaysDeny": "Erişimi Engelle", "passToAuth": "Kimlik Doğrulamasına Geç", "orgSettingsDescription": "Organizasyonunuzun genel ayarlarını yapılandırın", "orgGeneralSettings": "Organizasyon Ayarları", @@ -232,7 +234,7 @@ "orgMissing": "Organizasyon Kimliği Eksik", "orgMissingMessage": "Organizasyon kimliği olmadan daveti yeniden oluşturmanız mümkün değildir.", "accessUsersManage": "Kullanıcıları Yönet", - "accessUsersDescription": "Kullanıcıları davet edin ve erişimi yönetmek için rollere ekleyin", + "accessUsersDescription": "Bu organizasyona erişimi olan kullanıcıları davet edin ve yönetin", "accessUsersSearch": "Kullanıcıları ara...", "accessUserCreate": "Kullanıcı Oluştur", "accessUserRemove": "Kullanıcıyı Kaldır", @@ -241,13 +243,13 @@ "role": "Rol", "nameRequired": "Ad gereklidir", "accessRolesManage": "Rolleri Yönet", - "accessRolesDescription": "Organizasyonunuza erişimi yönetmek için rolleri yapılandırın", + "accessRolesDescription": "Organizasyondaki kullanıcılar için rolleri oluşturun ve yönetin", "accessRolesSearch": "Rolleri ara...", "accessRolesAdd": "Rol Ekle", "accessRoleDelete": "Rolü Sil", "description": "Açıklama", "inviteTitle": "Açık Davetiyeler", - "inviteDescription": "Davetiyelerinizi diğer kullanıcılarla yönetin", + "inviteDescription": "Organizasyona katılmak için diğer kullanıcılar için davetleri yönetin", "inviteSearch": "Davetiyeleri ara...", "minutes": "Dakika", "hours": "Saat", @@ -424,7 +426,7 @@ "userCreated": "Kullanıcı oluşturuldu", "userCreatedDescription": "Kullanıcı başarıyla oluşturulmuştur.", "userTypeInternal": "Dahili Kullanıcı", - "userTypeInternalDescription": "Kullanıcınızı doğrudan organizasyonunuza davet edin.", + "userTypeInternalDescription": "Kullanıcıyı doğrudan organizasyona davet edin.", "userTypeExternal": "Harici Kullanıcı", "userTypeExternalDescription": "Harici bir kimlik sağlayıcısıyla kullanıcı oluşturun.", "accessUserCreateDescription": "Yeni bir kullanıcı oluşturmak için aşağıdaki adımları izleyin", @@ -436,6 +438,16 @@ "inviteEmailSent": "Kullanıcıya davet e-postası gönder", "inviteValid": "Geçerli Süresi", "selectDuration": "Süreyi seçin", + "selectResource": "Kaynak Seçin", + "filterByResource": "Kaynağa Göre Filtrele", + "resetFilters": "Filtreleri Sıfırla", + "totalBlocked": "Pangolin Tarafından Engellenen İstekler", + "totalRequests": "Toplam İstekler", + "requestsByCountry": "Ülkeye Göre İstekler", + "requestsByDay": "Güne Göre İstekler", + "blocked": "Engellendi", + "allowed": "İzin Verildi", + "topCountries": "En İyi Ülkeler", "accessRoleSelect": "Rol seçin", "inviteEmailSentDescription": "Kullanıcıya erişim bağlantısı ile bir e-posta gönderildi. Daveti kabul etmek için bağlantıya erişmelidirler.", "inviteSentDescription": "Kullanıcı davet edilmiştir. Daveti kabul etmek için aşağıdaki bağlantıya erişmelidirler.", @@ -458,13 +470,13 @@ "accessControlsSubmit": "Erişim Kontrollerini Kaydet", "roles": "Roller", "accessUsersRoles": "Kullanıcılar ve Roller Yönetin", - "accessUsersRolesDescription": "Kullanıcılara davet gönderin ve organizasyonunuza erişim yönetmek için rollere ekleyin", + "accessUsersRolesDescription": "Kullanıcılara davet gönderin ve organizasyona erişimi yönetmek için rollere ekleyin", "key": "Anahtar", "createdAt": "Oluşturulma Tarihi", "proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.", "proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.", "proxyEnableSSL": "SSL Etkinleştir", - "proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.", + "proxyEnableSSLDescription": "Hedeflere güvenli HTTPS bağlantıları için SSL/TLS şifrelemesini etkinleştirin.", "target": "Hedef", "configureTarget": "Hedefleri Yapılandır", "targetErrorFetch": "Hedefleri alamadı", @@ -480,29 +492,29 @@ "targetsErrorUpdate": "Hedefler güncellenemedi", "targetsErrorUpdateDescription": "Hedefler güncellenirken bir hata oluştu", "targetTlsUpdate": "TLS ayarları güncellendi", - "targetTlsUpdateDescription": "TLS ayarlarınız başarıyla güncellendi", + "targetTlsUpdateDescription": "TLS ayarları başarıyla güncellendi", "targetErrorTlsUpdate": "TLS ayarları güncellenemedi", "targetErrorTlsUpdateDescription": "TLS ayarlarını güncellerken bir hata oluştu", "proxyUpdated": "Proxy ayarları güncellendi", - "proxyUpdatedDescription": "Proxy ayarlarınız başarıyla güncellenmiştir", + "proxyUpdatedDescription": "Proxy ayarları başarıyla güncellendi", "proxyErrorUpdate": "Proxy ayarları güncellenemedi", "proxyErrorUpdateDescription": "Proxy ayarlarını güncellerken bir hata oluştu", - "targetAddr": "IP / Hostname", + "targetAddr": "Host", "targetPort": "Bağlantı Noktası", "targetProtocol": "Protokol", "targetTlsSettings": "HTTPS & TLS Settings", - "targetTlsSettingsDescription": "Configure TLS settings for your resource", + "targetTlsSettingsDescription": "SSL/TLS ayarlarını kaynak için yapılandırın", "targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları", "targetTlsSni": "TLS Sunucu Adı", "targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'", "targetTlsSubmit": "Ayarları Kaydet", "targets": "Hedefler Konfigürasyonu", - "targetsDescription": "Trafiği arka uç hizmetlerinize yönlendirmek için hedefleri ayarlayın", + "targetsDescription": "Trafiği arka uç hizmetlerine 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ç", "targetSubmit": "Hedef Ekle", - "targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.", + "targetNoOne": "Bu kaynağın hedefleri yok. Arka uca gönderilecek istekleri yapılandırmak için bir hedef ekleyin.", "targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.", "targetsSubmit": "Hedefleri Kaydet", "addTarget": "Hedef Ekle", @@ -516,9 +528,11 @@ "targetCreatedDescription": "Hedef başarıyla oluşturuldu", "targetErrorCreate": "Hedef oluşturma başarısız oldu", "targetErrorCreateDescription": "Hedef oluşturulurken bir hata oluştu", + "tlsServerName": "TLS Sunucu Adı", + "tlsServerNameDescription": "SNI için kullanılacak TLS sunucu adı", "save": "Kaydet", "proxyAdditional": "Ek Proxy Ayarları", - "proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın", + "proxyAdditionalDescription": "Kaynağın proxy ayarlarını nasıl yöneteceği yapılandırın", "proxyCustomHeader": "Özel Ana Bilgisayar Başlığı", "proxyCustomHeaderDescription": "İstekleri proxy'lerken ayarlanacak ana bilgisayar başlığı. Varsayılanı kullanmak için boş bırakılır.", "proxyAdditionalSubmit": "Proxy Ayarlarını Kaydet", @@ -558,7 +572,7 @@ "rulesMatchType": "Eşleşme Türü", "value": "Değer", "rulesAbout": "Kurallar Hakkında", - "rulesAboutDescription": "Kurallar, kaynağınıza erişimi belirli bir kriterlere göre kontrol etmenizi sağlar. IP adresi veya URL yolu temelinde erişimi izin vermek veya engellemek için kurallar oluşturabilirsiniz.", + "rulesAboutDescription": "Kurallar, kaynağa erişimi belirli kriterlere göre kontrol etmenizi sağlar. IP adresi veya URL yolu temelinde erişimi izin vermek veya engellemek için kurallar oluşturabilirsiniz.", "rulesActions": "Aksiyonlar", "rulesActionAlwaysAllow": "Her Zaman İzin Ver: Tüm kimlik doğrulama yöntemlerini atlayın", "rulesActionAlwaysDeny": "Her Zaman Reddedin: Tüm istekleri engelleyin; kimlik doğrulaması yapılamaz", @@ -570,7 +584,7 @@ "rulesEnable": "Kuralları Etkinleştir", "rulesEnableDescription": "Bu kaynak için kural değerlendirmesini etkinleştirin veya devre dışı bırakın", "rulesResource": "Kaynak Kuralları Yapılandırması", - "rulesResourceDescription": "Kaynağınıza erişimi kontrol etmek için kuralları yapılandırın", + "rulesResourceDescription": "Kaynağa erişimi kontrol etmek için kuralları yapılandırın", "ruleSubmit": "Kural Ekle", "rulesNoOne": "Kural yok. Formu kullanarak bir kural ekleyin.", "rulesOrder": "Kurallar, artan öncelik sırasına göre değerlendirilir.", @@ -586,7 +600,7 @@ "none": "Hiçbiri", "unknown": "Bilinmiyor", "resources": "Kaynaklar", - "resourcesDescription": "Kaynaklar, özel ağınızda çalışan uygulamalara proxy görevi görür. Özel ağınızdaki herhangi bir HTTP/HTTPS veya ham TCP/UDP hizmeti için bir kaynak oluşturun. Her kaynak, şifreli bir WireGuard tüneli aracılığıyla özel ve güvenli bağlantıyı etkinleştirmek için bir siteye bağlı olmalıdır.", + "resourcesDescription": "Kaynaklar, özel ağda çalışan uygulamalara proxy olarak hizmet eder. Özel ağınızdaki herhangi bir HTTP/HTTPS veya ham TCP/UDP hizmeti için bir kaynak oluşturun. Her kaynak, şifreli bir WireGuard tüneli aracılığıyla özel, güvenli bağlanabilirliği etkinleştirmek için bir siteye bağlı olmalıdır.", "resourcesWireGuardConnect": "WireGuard şifreleme ile güvenli bağlantı", "resourcesMultipleAuthenticationMethods": "Birden fazla kimlik doğrulama yöntemi yapılandırın", "resourcesUsersRolesAccess": "Kullanıcı ve rol tabanlı erişim kontrolü", @@ -607,19 +621,19 @@ "unknownCommand": "Bilinmeyen komut", "newtErrorFetchReleases": "Sürüm bilgileri alınamadı: {err}", "newtErrorFetchLatest": "Son sürüm alınırken hata: {err}", - "newtEndpoint": "Newt Uç Noktası", - "newtId": "Newt Kimliği", - "newtSecretKey": "Newt Gizli Anahtarı", + "newtEndpoint": "Uç Nokta", + "newtId": "Kimlik", + "newtSecretKey": "Gizli", "architecture": "Mimari", "sites": "Siteler", - "siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklarınıza eş IP adresini kullanarak erişmeniz gerekecek.", + "siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.", "siteWgCompatibleAllClients": "Tüm WireGuard istemcileriyle uyumlu", "siteWgManualConfigurationRequired": "Manuel yapılandırma gerekli", "userErrorNotAdminOrOwner": "Kullanıcı yönetici veya sahibi değil", "pangolinSettings": "Ayarlar - Pangolin", "accessRoleYour": "Rolünüz:", - "accessRoleSelect2": "Bir rol seçin", - "accessUserSelect": "Bir kullanıcı seçin", + "accessRoleSelect2": "Rolleri seçin", + "accessUserSelect": "Kullanıcıları seçin", "otpEmailEnter": "Bir e-posta girin", "otpEmailEnterDescription": "E-posta girdikten sonra girdi alanına yazıp enter'a basın.", "otpEmailErrorInvalid": "Geçersiz e-posta adresi. Joker karakter (*) yerel kısmın tamamı olmalıdır.", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "Pincode Ayarla", "resourcePincodeSetupTitleDescription": "Bu kaynağı korumak için bir pincode ayarlayın", "resourceRoleDescription": "Yöneticiler her zaman bu kaynağa erişebilir.", - "resourceUsersRoles": "Kullanıcılar ve Roller", + "resourceUsersRoles": "Erişim Kontrolleri", "resourceUsersRolesDescription": "Bu kaynağı kimlerin ziyaret edebileceği kullanıcıları ve rolleri yapılandırın", "resourceUsersRolesSubmit": "Kullanıcıları ve Rolleri Kaydet", "resourceWhitelistSave": "Başarıyla kaydedildi", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "Kaynağı Aktar", "siteDestination": "Hedef Site", "searchSites": "Siteleri ara", + "countries": "Ülkeler", "accessRoleCreate": "Rol Oluştur", "accessRoleCreateDescription": "Kullanıcıları gruplamak ve izinlerini yönetmek için yeni bir rol oluşturun.", "accessRoleCreateSubmit": "Rol Oluştur", @@ -909,6 +924,10 @@ "passwordResetSent": "Bu e-posta adresine bir şifre sıfırlama kodu gönderilecektir.", "passwordResetCode": "Sıfırlama Kodu", "passwordResetCodeDescription": "E-posta gelen kutunuzda sıfırlama kodunu kontrol edin.", + "generatePasswordResetCode": "Parola Sıfırlama Kodunu Oluştur", + "passwordResetCodeGenerated": "Parola Sıfırlama Kodu Oluşturuldu", + "passwordResetCodeGeneratedDescription": "Bu kodu kullanıcı ile paylaşın. Parolalarını sıfırlamak için bunu kullanabilirler.", + "passwordResetUrl": "Parola Sıfırlama URL'si", "passwordNew": "Yeni Şifre", "passwordNewConfirm": "Yeni Şifreyi Onayla", "changePassword": "Parola Değiştir", @@ -926,6 +945,9 @@ "pincodeAuth": "Kimlik Doğrulama Kodu", "pincodeSubmit2": "Kodu Gönder", "passwordResetSubmit": "Sıfırlama İsteği", + "passwordResetAlreadyHaveCode": "Parola Sıfırlama Kodunu Giriniz", + "passwordResetSmtpRequired": "Yönetici ile iletişime geçin", + "passwordResetSmtpRequiredDescription": "Parolanızı sıfırlamak için bir parola sıfırlama kodu gereklidir. Yardım için yönetici ile iletişime geçin.", "passwordBack": "Şifreye Geri Dön", "loginBack": "Girişe geri dön", "signup": "Kaydol", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "Site Kaynaklarını Listele", "actionUpdateSiteResource": "Site Kaynağını Güncelle", "actionListInvitations": "Davetiyeleri Listele", + "actionExportLogs": "Kayıtları Dışa Aktar", + "actionViewLogs": "Kayıtları Görüntüle", "noneSelected": "Hiçbiri seçili değil", "orgNotFound2": "Hiçbir organizasyon bulunamadı.", "searchProgress": "Ara...", "create": "Oluştur", "orgs": "Organizasyonlar", "loginError": "Giriş yaparken bir hata oluştu", + "loginRequiredForDevice": "Cihazınızı kimlik doğrulamak için giriş yapılması gereklidir.", "passwordForgot": "Şifrenizi mi unuttunuz?", "otpAuth": "İki Faktörlü Kimlik Doğrulama", "otpAuthDescription": "Authenticator uygulamanızdan veya tek kullanımlık yedek kodlarınızdan birini girin.", @@ -1151,19 +1176,29 @@ "sidebarHome": "Ana Sayfa", "sidebarSites": "Siteler", "sidebarResources": "Kaynaklar", + "sidebarProxyResources": "Herkese Açık", + "sidebarClientResources": "Özel", "sidebarAccessControl": "Erişim Kontrolü", + "sidebarLogsAndAnalytics": "Kayıtlar & Analitik", "sidebarUsers": "Kullanıcılar", + "sidebarAdmin": "Yönetici", "sidebarInvitations": "Davetiye", "sidebarRoles": "Roller", - "sidebarShareableLinks": "Paylaşılabilir Bağlantılar", + "sidebarShareableLinks": "Bağlantılar", "sidebarApiKeys": "API Anahtarları", "sidebarSettings": "Ayarlar", "sidebarAllUsers": "Tüm Kullanıcılar", "sidebarIdentityProviders": "Kimlik Sağlayıcılar", "sidebarLicense": "Lisans", "sidebarClients": "İstemciler", + "sidebarUserDevices": "Kullanıcılar", + "sidebarMachineClients": "Makineler", "sidebarDomains": "Alan Adları", + "sidebarGeneral": "Genel", + "sidebarLogAndAnalytics": "Kayıt & Analiz", "sidebarBluePrints": "Planlar", + "sidebarOrganization": "Organizasyon", + "sidebarLogsAnalytics": "Analitik", "blueprints": "Planlar", "blueprintsDescription": "Deklaratif yapılandırmaları uygulayın ve önceki çalışmaları görüntüleyin", "blueprintAdd": "Plan Ekle", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "Uygulanan mavi yazılımın sonucunu ve oluşan hataları görün", "blueprintInfo": "Plan Bilgileri", "message": "Mesaj", - "blueprintContentsDescription": "Altyapınızı tanımlayan YAML içeriğini tanımlayın", + "blueprintContentsDescription": "Altyapıyı tanımlayan YAML içeriğini belirleyin", "blueprintErrorCreateDescription": "Plan uygulanırken bir hata oluştu", "blueprintErrorCreate": "Plan oluşturulurken hata oluştu", "searchBlueprintProgress": "Planlarda ara...", @@ -1230,15 +1265,15 @@ "loading": "Yükleniyor", "restart": "Yeniden Başlat", "domains": "Alan Adları", - "domainsDescription": "Organizasyonunuz için alan adlarını yönetin", + "domainsDescription": "Organizasyonda kullanılabilir alan adlarını oluşturun ve yönetin", "domainsSearch": "Alan adlarını ara...", "domainAdd": "Alan Adı Ekle", "domainAddDescription": "Organizasyonunuz için yeni bir alan adı kaydedin", "domainCreate": "Alan Adı Oluştur", "domainCreatedDescription": "Alan adı başarıyla oluşturuldu", "domainDeletedDescription": "Alan adı başarıyla silindi", - "domainQuestionRemove": "Alan adını hesabınızdan kaldırmak istediğinizden emin misiniz?", - "domainMessageRemove": "Kaldırıldığında, alan adı hesabınızla ilişkilendirilmeyecek.", + "domainQuestionRemove": "Alan adını kaldırmak istediğinizden emin misiniz?", + "domainMessageRemove": "Kaldırıldığında, alan adı artık organizasyonunuzla ilişkilendirilmez.", "domainConfirmDelete": "Alan Adı Silinmesini Onayla", "domainDelete": "Alan Adını Sil", "domain": "Alan Adı", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "Ürün Güncellemeleri", "productUpdateEmpty": "Güncelleme yok", "dismissAll": "Hepsini Kapat", - "pangolinUpdateAvailable": "Yeni sürüm mevcut", + "pangolinUpdateAvailable": "Güncelleme Mevcut", "pangolinUpdateAvailableInfo": "Sürüm {version} yüklenmeye hazır", - "pangolinUpdateAvailableReleaseNotes": "Sürüm notlarını görüntüleyin", + "pangolinUpdateAvailableReleaseNotes": "Yayın Notlarını Görüntüle", "newtUpdateAvailable": "Güncelleme Mevcut", "newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.", "domainPickerEnterDomain": "Alan Adı", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Kullanılabilirlik kontrol ediliyor...", - "domainPickerNoMatchingDomains": "Eşleşen domain bulunamadı. Farklı bir domain deneyin veya organizasyonunuzun domain ayarlarını kontrol edin.", + "domainPickerNoMatchingDomains": "Eşleşen alan adı bulunamadı. Farklı bir alan adı deneyin veya organizasyonunuzun alan ayarlarını kontrol edin.", "domainPickerOrganizationDomains": "Organizasyon Alan Adları", "domainPickerProvidedDomains": "Sağlanan Alan Adları", "domainPickerSubdomain": "Alt Alan: {subdomain}", @@ -1345,9 +1380,9 @@ "billingPortalError": "Portal Hatası", "billingDataUsageInfo": "Buluta bağlandığınızda, güvenli tünellerinizden aktarılan tüm verilerden ücret alınırsınız. Bu, tüm sitelerinizdeki gelen ve giden trafiği içerir. Limitinize ulaştığınızda, planınızı yükseltmeli veya kullanımı azaltmalısınız, aksi takdirde siteleriniz bağlantıyı keser. Düğümler kullanırken verilerden ücret alınmaz.", "billingOnlineTimeInfo": "Sitelerinizin buluta ne kadar süre bağlı kaldığına göre ücretlendirilirsiniz. Örneğin, 44,640 dakika, bir sitenin 24/7 boyunca tam bir ay boyunca çalışması anlamına gelir. Limitinize ulaştığınızda, planınızı yükseltmeyip kullanımı azaltmazsanız siteleriniz bağlantıyı keser. Düğümler kullanırken zamandan ücret alınmaz.", - "billingUsersInfo": "Kuruluşunuzdaki her kullanıcı için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif kullanıcı hesaplarının sayısına göre günlük olarak hesaplanır.", - "billingDomainInfo": "Kuruluşunuzdaki her alan adı için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif alan adları hesaplarının sayısına göre günlük olarak hesaplanır.", - "billingRemoteExitNodesInfo": "Kuruluşunuzdaki her yönetilen Düğüm için ücretlendirilirsiniz. Faturalandırma, hesabınızdaki aktif yönetilen Düğümler sayısına göre günlük olarak hesaplanır.", + "billingUsersInfo": "Kuruluşunuzdaki her kullanıcı için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif kullanıcı hesaplarının sayısına göre günlük olarak hesaplanır.", + "billingDomainInfo": "Kuruluşunuzdaki her alan adı için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif alan adları hesaplarının sayısına göre günlük olarak hesaplanır.", + "billingRemoteExitNodesInfo": "Kuruluşunuzdaki her yönetilen Düğüm için ücretlendirilirsiniz. Faturalandırma, organizasyonunuza kayıtlı aktif yönetilen Düğümler sayısına göre günlük olarak hesaplanır.", "domainNotFound": "Alan Adı Bulunamadı", "domainNotFoundDescription": "Bu kaynak devre dışıdır çünkü alan adı sistemimizde artık mevcut değil. Bu kaynak için yeni bir alan adı belirleyin.", "failed": "Başarısız", @@ -1430,28 +1465,31 @@ "and": "ve", "privacyPolicy": "gizlilik politikası" }, + "signUpMarketing": { + "keepMeInTheLoop": "Bana e-posta yoluyla haberler, güncellemeler ve yeni özellikler hakkında bilgi verin." + }, "siteRequired": "Site gerekli.", "olmTunnel": "Olm Tüneli", "olmTunnelDescription": "Müşteri bağlantıları için Olm kullanın", "errorCreatingClient": "Müşteri oluşturulurken hata oluştu", "clientDefaultsNotFound": "Müşteri varsayılanları bulunamadı", "createClient": "Müşteri Oluştur", - "createClientDescription": "Sitelerinize bağlanmak için yeni bir müşteri oluşturun", + "createClientDescription": "Özel kaynaklara erişmek için yeni bir istemci oluşturun", "seeAllClients": "Tüm Müşterileri Gör", "clientInformation": "Müşteri Bilgileri", "clientNamePlaceholder": "Müşteri adı", "address": "Adres", "subnetPlaceholder": "Alt ağ", - "addressDescription": "Bu müşteri için bağlantıda kullanılacak adres", + "addressDescription": "İstemcinin dahili adresi. Organizasyon alt ağı içinde olmalıdır.", "selectSites": "Siteleri seçin", "sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır", "clientInstallOlm": "Olm Yükle", "clientInstallOlmDescription": "Sisteminizde Olm çalıştırın", "clientOlmCredentials": "Olm Kimlik Bilgileri", - "clientOlmCredentialsDescription": "Bu, Olm'in sunucu ile kimlik doğrulaması yapacağı yöntemdir", - "olmEndpoint": "Olm Uç Noktası", - "olmId": "Olm Kimliği", - "olmSecretKey": "Olm Gizli Anahtarı", + "clientOlmCredentialsDescription": "Bu, istemcinin sunucu ile kimlik doğrulaması yapacağı yöntemdir", + "olmEndpoint": "Uç Nokta", + "olmId": "Kimlik", + "olmSecretKey": "Gizli", "clientCredentialsSave": "Kimlik Bilgilerinizi Kaydedin", "clientCredentialsSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.", "generalSettingsDescription": "Bu müşteri için genel ayarları yapılandırın", @@ -1463,9 +1501,7 @@ "sitesFetchError": "Siteler alınırken bir hata oluştu.", "olmErrorFetchReleases": "Olm yayınları alınırken bir hata oluştu.", "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ş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", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "Bu hedefin sağlığını izleyin. Gerekirse hedef dışındaki bir son noktayı izleyebilirsiniz.", "healthScheme": "Yöntem", "healthSelectScheme": "Yöntem Seç", + "healthCheckPortInvalid": "Sağlık Kontrolü portu 1 ile 65535 arasında olmalıdır", "healthCheckPath": "Yol", "healthHostname": "IP / Hostname", "healthPort": "Bağlantı Noktası", "healthCheckPathDescription": "Sağlık durumunu kontrol etmek için yol.", - "healthyIntervalSeconds": "Sağlıklı Aralık", - "unhealthyIntervalSeconds": "Sağlıksız Aralık", + "healthyIntervalSeconds": "Sağlıklı Aralık (saniye)", + "unhealthyIntervalSeconds": "Sağlıksız Aralık (saniye)", "IntervalSeconds": "Sağlıklı Aralık", - "timeoutSeconds": "Zaman Aşımı", + "timeoutSeconds": "Zaman Aşımı (saniye)", "timeIsInSeconds": "Zaman saniye cinsindendir", "retryAttempts": "Tekrar Deneme Girişimleri", "expectedResponseCodes": "Beklenen Yanıt Kodları", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "Alan Adını Düzenle", "siteName": "Site Adı", "proxyPort": "Bağlantı Noktası", - "resourcesTableProxyResources": "Proxy Kaynaklar", - "resourcesTableClientResources": "İstemci Kaynaklar", + "resourcesTableProxyResources": "Herkese Açık", + "resourcesTableClientResources": "Özel", "resourcesTableNoProxyResourcesFound": "Hiçbir proxy kaynağı bulunamadı.", "resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.", "resourcesTableDestination": "Hedef", - "resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için", + "resourcesTableAlias": "Takma Ad", "resourcesTableClients": "İstemciler", "resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.", "resourcesTableNoTargets": "Hedef yok", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "Çevrimdışı", "resourcesTableUnknown": "Bilinmiyor", "resourcesTableNotMonitored": "İzlenmiyor", - "editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin", - "editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.", + "editInternalResourceDialogEditClientResource": "Özel Kaynak Düzenleyin", + "editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak ayarlarını ve erişim kontrollerini güncelleyin", "editInternalResourceDialogResourceProperties": "Kaynak Özellikleri", "editInternalResourceDialogName": "Ad", "editInternalResourceDialogProtocol": "Protokol", @@ -1560,11 +1597,22 @@ "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", + "editInternalResourceDialogPortModeRequired": "Port modu için protokol, proxy portu ve hedef porta ihtiyaç vardır", + "editInternalResourceDialogMode": "Mod", + "editInternalResourceDialogModePort": "Bağlantı Noktası", + "editInternalResourceDialogModeHost": "Ev Sahibi", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "Hedef", + "editInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.", + "editInternalResourceDialogDestinationIPDescription": "Kaynağın site ağındaki IP veya ana bilgisayar adresi.", + "editInternalResourceDialogDestinationCidrDescription": "Site ağındaki kaynağın CIDR aralığı.", + "editInternalResourceDialogAlias": "Takma Ad", + "editInternalResourceDialogAliasDescription": "Bu kaynak için isteğe bağlı dahili DNS takma adı.", "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.", + "createInternalResourceDialogCreateClientResource": "Özel Kaynak Oluştur", + "createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemcilere erişilebilir olacak yeni bir kaynak oluşturun", "createInternalResourceDialogResourceProperties": "Kaynak Özellikleri", "createInternalResourceDialogName": "Ad", "createInternalResourceDialogSite": "Site", @@ -1593,11 +1641,22 @@ "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", + "createInternalResourceDialogPortModeRequired": "Port modu için protokol, proxy portu ve hedef porta ihtiyaç vardır", + "createInternalResourceDialogMode": "Mod", + "createInternalResourceDialogModePort": "Bağlantı Noktası", + "createInternalResourceDialogModeHost": "Ev Sahibi", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "Hedef", + "createInternalResourceDialogDestinationHostDescription": "Site ağındaki kaynağın IP adresi veya ana bilgisayar adı.", + "createInternalResourceDialogDestinationCidrDescription": "Site ağındaki kaynağın CIDR aralığı.", + "createInternalResourceDialogAlias": "Takma Ad", + "createInternalResourceDialogAliasDescription": "Bu kaynak için isteğe bağlı dahili DNS takma adı.", "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.", + "siteAcceptClientConnectionsDescription": "Kullanıcı cihazları ve istemcilerin bu sitedeki kaynaklara erişmesine izin verin. Bu daha sonra değiştirilebilir.", + "siteAddress": "Site Adresi (Gelişmiş)", + "siteAddressDescription": "Site için dahili adres. Organizasyon alt ağı içinde olmalıdır.", + "siteNameDescription": "Sonradan değiştirilebilecek sitenin görünen adı.", "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ç", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.", "autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.", "remoteExitNodeManageRemoteExitNodes": "Uzak Düğümler", - "remoteExitNodeDescription": "Kendi konum ağlarınızdan bir veya daha fazlasını barındırarak, bağlantı kurulumları için buluta bağımlılığı azaltın.", + "remoteExitNodeDescription": "Ağ bağlantınızı genişletmek ve buluta bağlı kalmayı azaltmak için bir veya daha fazla uzak düğüm barındırın", "remoteExitNodes": "Düğümler", "searchRemoteExitNodes": "Düğüm ara...", "remoteExitNodeAdd": "Düğüm Ekle", @@ -1648,7 +1707,7 @@ }, "generate": { "title": "Oluşturulan Kimlik Bilgileri", - "description": "Düğümünüzü yapılandırmak için oluşturulan bu kimlik bilgilerini kullanın", + "description": "Düğümünüzü yapılandırmak için bu oluşturulan kimlik bilgilerini kullanın", "nodeIdTitle": "Düğüm ID", "secretTitle": "Gizli", "saveCredentialsTitle": "Kimlik Bilgilerini Yapılandırmaya Ekle", @@ -1730,7 +1789,7 @@ "idpAzureConfiguration": "Azure Entra ID Yapılandırması", "idpAzureConfigurationDescription": "Azure Entra ID OAuth2 kimlik bilgilerinizi yapılandırın", "idpTenantId": "Kiracı Kimliği", - "idpTenantIdPlaceholder": "kiraci-kimliginiz", + "idpTenantIdPlaceholder": "kiracı Kimliği", "idpAzureTenantIdDescription": "Azure kiracı kimliğiniz (Azure Active Directory genel bakışında bulunur)", "idpAzureClientIdDescription": "Azure Uygulama Kaydı İstemci Kimliğiniz", "idpAzureClientSecretDescription": "Azure Uygulama Kaydı İstemci Sırrınız", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "Kurumsal Sürüm", "unlicensed": "Lisansız", "beta": "Beta", - "manageClients": "Müşteri Yönetimi", - "manageClientsDescription": "Müşteriler, sitelerinize bağlanabilen cihazlardır.", + "manageUserDevices": "Kullanıcı Cihazları", + "manageUserDevicesDescription": "Kullanıcıların kaynaklara özel olarak bağlanmak için kullandığı cihazları görüntüleyin ve yönetin", + "manageMachineClients": "Makine İstemcilerini Yönetin", + "manageMachineClientsDescription": "Sunucuların ve sistemlerin kaynaklara özel olarak bağlanmak için kullandığı istemcileri oluşturun ve yönetin", + "clientsTableUserClients": "Kullanıcı", + "clientsTableMachineClients": "Makine", "licenseTableValidUntil": "Geçerli İki Tarih Kadar", "saasLicenseKeysSettingsTitle": "Kurumsal Lisanslar", "saasLicenseKeysSettingsDescription": "Kendi barındırdığınız Pangolin örnekleri için kurumsal lisans anahtarları oluşturun ve yönetin.", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "sil", "sidebarEnableEnterpriseLicense": "Kurumsal Lisans Etkinleştir", "cannotbeUndone": "Bu geri alınamaz.", - "toConfirm": "doğrulamak için", + "toConfirm": "onaylamak için.", "deleteClientQuestion": "Müşteriyi siteden ve organizasyondan kaldırmak istediğinizden emin misiniz?", "clientMessageRemove": "Kaldırıldıktan sonra müşteri siteye bağlanamayacaktır.", "sidebarLogs": "Kayıtlar", "request": "İstek", + "requests": "İstekler", "logs": "Günlükler", "logsSettingsDescription": "Bu organizasyondan toplanan günlükleri izleyin", "searchLogs": "Günlüklerde ara...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "Sebep", "requestLogs": "İstek Günlükleri", + "requestAnalytics": "İstek Analizi", "host": "Sunucu", "location": "Konum", "actionLogs": "Eylem Günlükleri", @@ -2029,6 +2094,7 @@ "logRetention": "Kayıt Saklama", "logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın", "requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin", + "requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.", "logRetentionRequestLabel": "İstek Günlüğü Saklama", "logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle", "logRetentionAccessLabel": "Erişim Günlüğü Saklama", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 gün", "logRetention90Days": "90 gün", "logRetentionForever": "Sonsuza kadar", + "logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu", "actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin", "accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin", "licenseRequiredToUse": "Bu özelliği kullanmak için bir kurumsal lisans gereklidir.", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "Joker Sertifikayı Tercih Et", "unverified": "Doğrulanmadı", "domainSetting": "Alan Adı Ayarları", - "domainSettingDescription": "Alan adınız için ayarları yapılandırın", + "domainSettingDescription": "Alan adı için ayarları yapılandırın", "preferWildcardCertDescription": "Joker sertifika üretmeye çalışın (doğru yapılandırılmış bir sertifika çözücü gereklidir).", "recordName": "Kayıt Adı", "auto": "Otomatik", @@ -2066,9 +2133,9 @@ "olmUpdateAvailableInfo": "Olm'nin güncellenmiş bir sürümü mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.", "client": "İstemci", "proxyProtocol": "Proxy Protokol Ayarları", - "proxyProtocolDescription": "TCP/UDP hizmetleri için istemci IP adreslerini korumak için Proxy Protokolünü yapılandırın.", + "proxyProtocolDescription": "TCP hizmetleri için istemci IP adreslerini korumak amacıyla Proxy Protokolünü yapılandırın.", "enableProxyProtocol": "Proxy Protokolünü Etkinleştir", - "proxyProtocolInfo": "TCP/UDP arka uçları için istemci IP adreslerini koruyun", + "proxyProtocolInfo": "TCP ara yüzlerini koruyarak istemci IP adreslerini saklayın", "proxyProtocolVersion": "Proxy Protokol Versiyonu", "version1": " Versiyon 1 (Önerilen)", "version2": "Versiyon 2", @@ -2097,6 +2164,43 @@ "supportMessageSent": "Mesaj Gönderildi!", "supportWillContact": "En kısa sürede size geri döneceğiz!", "selectLogRetention": "Kayıt saklama seç", + "terms": "Şartlar", + "privacy": "Gizlilik", + "security": "Güvenlik", + "docs": "Belgeler", + "deviceActivation": "Cihaz aktivasyonu", + "deviceCodeInvalidFormat": "Kod 9 karakter olmalı (ör. A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "Geçersiz veya süresi dolmuş kod", + "deviceCodeVerifyFailed": "Cihaz kodu doğrulanamadı", + "signedInAs": "Olarak giriş yapıldı", + "deviceCodeEnterPrompt": "Cihazda gösterilen kodu girin", + "continue": "Devam Et", + "deviceUnknownLocation": "Bilinmeyen konum", + "deviceAuthorizationRequested": "Bu yetkilendirme {tarih} tarihinde {konum} konumundan talep edildi. Bu cihaza güvenmenizi sağlayın, çünkü hesap erişimine sahip olacaktır.", + "deviceLabel": "Cihaz: {cihazadı}", + "deviceWantsAccess": "hesabınıza erişmek istiyor", + "deviceExistingAccess": "Mevcut erişim:", + "deviceFullAccess": "Hesabınıza tam erişim", + "deviceOrganizationsAccess": "Hesabınızın erişim hakkına sahip olduğu tüm organizasyonlara erişim", + "deviceAuthorize": "{uygulamaAdi} yetkilendir", + "deviceConnected": "Cihaz Bağlandı!", + "deviceAuthorizedMessage": "Cihazınız, hesabınıza erişim izni almıştır.", + "pangolinCloud": "Pangolin Cloud", + "viewDevices": "Cihazları Görüntüle", + "viewDevicesDescription": "Bağlantılı cihazlarınızı yönetin", + "noDevices": "Cihaz bulunamadı", + "dateCreated": "Oluşturulma Tarihi", + "unnamedDevice": "Adı Olmayan Cihaz", + "deviceQuestionRemove": "Bu cihazı silmek istediğinizden emin misiniz?", + "deviceMessageRemove": "Bu eylem geri alınamaz.", + "deviceDeleteConfirm": "Cihazı Sil", + "deleteDevice": "Cihazı Sil", + "errorLoadingDevices": "Cihaz yüklenirken hata oluştu", + "failedToLoadDevices": "Cihazlar yüklenemedi", + "deviceDeleted": "Cihaz silindi", + "deviceDeletedDescription": "Cihaz başarıyla silindi.", + "errorDeletingDevice": "Cihaz silinirken hata", + "failedToDeleteDevice": "Cihaz silinirken hata oluştu", "showColumns": "Sütunları Göster", "hideColumns": "Sütunları Gizle", "columnVisibility": "Sütun Görünürlüğü", @@ -2111,10 +2215,14 @@ "enableSelected": "Seçilenleri Etkinleştir", "disableSelected": "Seçilenleri Devre Dışı Bırak", "checkSelectedStatus": "Seçilenlerin Durumunu Kontrol Et", + "clients": "İstemciler", + "accessClientSelect": "Makine istemcilerini seçiniz", + "resourceClientDescription": "Bu kaynağa erişebilecek makine istemcileri", + "regenerate": "Yeniden Üret", "credentials": "Kimlik Bilgileri", "savecredentials": "Kimlik Bilgilerini Kaydet", - "regeneratecredentials": "Yeniden Anahtarla", - "regenerateCredentials": "Kimlik bilgilerinizi yeniden oluşturun ve kaydedin", + "regenerateCredentialsButton": "Kimlik Bilgilerini Yeniden Oluştur", + "regenerateCredentials": "Kimlik Bilgilerini Yeniden Oluştur", "generatedcredentials": "Oluşturulan Kimlik Bilgileri", "copyandsavethesecredentials": "Bu kimlik bilgilerini kopyalayın ve kaydedin", "copyandsavethesecredentialsdescription": "Bu sayfadan ayrıldıktan sonra bu kimlik bilgileri tekrar gösterilmeyecek. Onları şimdi güvenli bir şekilde saklayın.", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "Kimlik bilgileri başarılı bir şekilde yeniden oluşturuldu ve kaydedildi.", "credentialsSaveError": "Kimlik Bilgileri Kayıt Hatası", "credentialsSaveErrorDescription": "Kimlik bilgilerini yeniden oluştururken ve kaydederken bir hata oluştu.", - "regenerateCredentialsWarning": "Kimlik bilgilerini yeniden oluşturmak önceki bilgileri geçersiz kılacaktır. Bu kimlik bilgilerini kullanan tüm yapılandırmaları güncellediğinizden emin olun.", + "regenerateCredentialsWarning": "Kimlik bilgilerini yeniden oluşturmak, öncekilerini geçersiz kılacak ve bağlantı kesintisine neden olacaktır. Bu kimlik bilgilerini kullanan yapılandırmaları güncellediğinizden emin olun.", "confirm": "Onayla", "regenerateCredentialsConfirmation": "Kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?", "endpoint": "Uç Nokta", "Id": "Kimlik", "SecretKey": "Gizli Anahtar", - "featureDisabledTooltip": "Bu özellik yalnızca kurumsal planda mevcuttur ve kullanmak için lisans gerektirir.", "niceId": "Güzel Kimlik", "niceIdUpdated": "Güzel Kimlik Güncellendi", "niceIdUpdatedSuccessfully": "Güzel Kimlik Başarıyla Güncellendi", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "Güzel Kimlik güncellenirken bir hata oluştu.", "niceIdCannotBeEmpty": "Güzel Kimlik boş olamaz", "enterIdentifier": "Tanımlayıcıyı girin", - "identifier": "Tanımlayıcı" + "identifier": "Tanımlayıcı", + "deviceLoginUseDifferentAccount": "Siz değil misiniz? Farklı bir hesap kullanın.", + "deviceLoginDeviceRequestingAccessToAccount": "Bir cihaz bu hesaba erişim talep ediyor.", + "noData": "Veri Yok", + "machineClients": "Makine İstemcileri", + "install": "Yükle", + "run": "Çalıştır", + "clientNameDescription": "Daha sonra değiştirilebilecek istemcinin görünen adı.", + "clientAddress": "İstemci Adresi (Gelişmiş)", + "setupFailedToFetchSubnet": "Varsayılan alt ağ alınamadı", + "setupSubnetAdvanced": "Alt Ağ (Gelişmiş)", + "setupSubnetDescription": "Bu organizasyonun dahili ağı için alt ağ.", + "siteRegenerateAndDisconnect": "Yeniden Oluştur ve Bağlantıyı Kes", + "siteRegenerateAndDisconnectConfirmation": "Kimlik bilgilerini yeniden oluşturmak ve bu sitenin bağlantısını kesmek istediğinizden emin misiniz?", + "siteRegenerateAndDisconnectWarning": "Bu, kimlik bilgilerini yeniden oluşturacak ve sitenin bağlantısını anında kesecektir. Site yeni kimlik bilgilerle yeniden başlatılmalıdır.", + "siteRegenerateCredentialsConfirmation": "Bu site için kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?", + "siteRegenerateCredentialsWarning": "Bu, kimlik bilgilerini yeniden oluşturacak. Site manuel olarak yeniden başlatılana ve yeni kimlik bilgileri kullanılana kadar bağlı kalacak.", + "clientRegenerateAndDisconnect": "Yeniden Oluştur ve Bağlantıyı Kes", + "clientRegenerateAndDisconnectConfirmation": "Kimlik bilgilerini yeniden oluşturmak ve bu istemcinin bağlantısını kesmek istediğinizden emin misiniz?", + "clientRegenerateAndDisconnectWarning": "Bu, kimlik bilgilerini yeniden oluşturacak ve istemcinin bağlantısını hemen kesecek. İstemci, yeni kimlik bilgilerle yeniden başlatılmalıdır.", + "clientRegenerateCredentialsConfirmation": "Bu istemci için kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?", + "clientRegenerateCredentialsWarning": "Bu, kimlik bilgilerini yeniden oluşturacak. İstemci, manuel olarak yeniden başlatılana ve yeni kimlik bilgileri kullanılana kadar bağlı kalacak.", + "remoteExitNodeRegenerateAndDisconnect": "Yeniden Oluştur ve Bağlantıyı Kes", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Kimlik bilgilerini yeniden oluşturmak ve bu uzak çıkış düğümünün bağlantısını kesmek istediğinizden emin misiniz?", + "remoteExitNodeRegenerateAndDisconnectWarning": "Bu, kimlik bilgilerini yeniden oluşturacak ve hemen uzak çıkış düğümünün bağlantısını kesecek. Uzak çıkış düğümü, yeni kimlik bilgileri ile yeniden başlatılmalıdır.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Bu uzak çıkış düğümü için kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?", + "remoteExitNodeRegenerateCredentialsWarning": "Bu, kimlik bilgilerini yeniden oluşturacak. Uzak çıkış düğümü, manuel olarak yeniden başlatılana ve yeni kimlik bilgiler kullanılana kadar bağlı kalacak.", + "agent": "Aracı" } diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 6b13a912..b8e1f2b1 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -1,12 +1,12 @@ { - "setupCreate": "创建您的第一个组织、网站和资源", + "setupCreate": "创建组织、站点和资源", "setupNewOrg": "新建组织", "setupCreateOrg": "创建组织", "setupCreateResources": "创建资源", "setupOrgName": "组织名称", - "orgDisplayName": "这是您组织的显示名称。", + "orgDisplayName": "这是组织的显示名称。", "orgId": "组织ID", - "setupIdentifierMessage": "这是您组织的唯一标识符。这是与显示名称分开的。", + "setupIdentifierMessage": "这是组织唯一的标识符。", "setupErrorIdentifier": "组织ID 已被使用。请另选一个。", "componentsErrorNoMemberCreate": "您目前不是任何组织的成员。创建组织以开始操作。", "componentsErrorNoMember": "您目前不是任何组织的成员。", @@ -50,10 +50,10 @@ "siteMessageRemove": "一旦移除,站点将无法访问。与站点相关的所有目标也将被移除。", "siteQuestionRemove": "您确定要从组织中删除该站点吗?", "siteManageSites": "管理站点", - "siteDescription": "允许通过安全隧道连接到您的网络", + "siteDescription": "创建和管理站点,启用与私人网络的连接", "siteCreate": "创建站点", "siteCreateDescription2": "按照下面的步骤创建和连接一个新站点", - "siteCreateDescription": "创建一个新站点开始连接您的资源", + "siteCreateDescription": "创建一个新站点开始连接资源", "close": "关闭", "siteErrorCreate": "创建站点出错", "siteErrorCreateKeyPair": "找不到密钥对或站点默认值", @@ -74,7 +74,7 @@ "siteInstallNewt": "安装 Newt", "siteInstallNewtDescription": "在您的系统中运行 Newt", "WgConfiguration": "WireGuard 配置", - "WgConfigurationDescription": "使用以下配置连接到您的网络", + "WgConfigurationDescription": "使用以下配置连接到网络", "operatingSystem": "操作系统", "commands": "命令", "recommended": "推荐", @@ -87,32 +87,32 @@ "siteUpdated": "站点已更新", "siteUpdatedDescription": "网站已更新。", "siteGeneralDescription": "配置此站点的常规设置", - "siteSettingDescription": "配置您网站上的设置", + "siteSettingDescription": "配置站点设置", "siteSetting": "{siteName} 设置", - "siteNewtTunnel": "Newt 隧道 (推荐)", - "siteNewtTunnelDescription": "最简单的方式来连接到您的网络。不需要任何额外设置。", + "siteNewtTunnel": "新站点 (推荐)", + "siteNewtTunnelDescription": "最简单的方式来创建任何网络的入口。没有额外的设置。", "siteWg": "基本 WireGuard", "siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。", "siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。", "siteLocalDescription": "仅限本地资源。不需要隧道。", "siteLocalDescriptionSaas": "仅本地资源。没有隧道。仅在远程节点上可用。", "siteSeeAll": "查看所有站点", - "siteTunnelDescription": "确定如何连接到您的网站", - "siteNewtCredentials": "Newt 凭据", - "siteNewtCredentialsDescription": "这是 Newt 服务器的身份验证凭据", - "siteCredentialsSave": "保存您的凭据", + "siteTunnelDescription": "确定如何连接到站点", + "siteNewtCredentials": "全权证书", + "siteNewtCredentialsDescription": "站点如何通过服务器进行身份验证", + "siteCredentialsSave": "保存证书", "siteCredentialsSaveDescription": "您只能看到一次。请确保将其复制并保存到一个安全的地方。", "siteInfo": "站点信息", "status": "状态", "shareTitle": "管理共享链接", - "shareDescription": "创建可共享的链接,允许暂时或永久访问您的资源", + "shareDescription": "创建可共享的链接,允许临时或永久访问代理资源", "shareSearch": "搜索共享链接...", "shareCreate": "创建共享链接", "shareErrorDelete": "删除链接失败", "shareErrorDeleteMessage": "删除链接时出错", "shareDeleted": "链接已删除", "shareDeletedDescription": "链接已删除", - "shareTokenDescription": "您的访问令牌可以通过两种方式传递:作为查询参数或请求头。 每次验证访问请求都必须从客户端传递。", + "shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。", "accessToken": "访问令牌", "usageExamples": "用法示例", "tokenId": "令牌 ID", @@ -121,7 +121,7 @@ "importantNote": "重要提示", "shareImportantDescription": "出于安全考虑,建议尽可能在使用请求头传递参数,因为查询参数可能会被浏览器历史记录或服务器日志记录。", "token": "令牌", - "shareTokenSecurety": "请妥善保管您的访问令牌,不要将其暴露在公开访问的区域或客户端代码中。", + "shareTokenSecurety": "保持访问令牌的安全。请不要在公开可访问的区域或客户端代码中共享它。", "shareErrorFetchResource": "获取资源失败", "shareErrorFetchResourceDescription": "获取资源时出错", "shareErrorCreate": "无法创建共享链接", @@ -144,8 +144,10 @@ "expires": "过期时间", "never": "永不过期", "shareErrorSelectResource": "请选择一个资源", - "resourceTitle": "管理资源", - "resourceDescription": "为您的私人应用程序创建安全代理", + "proxyResourceTitle": "管理公共资源", + "proxyResourceDescription": "创建和管理可通过 Web 浏览器公开访问的资源", + "clientResourceTitle": "管理私有资源", + "clientResourceDescription": "创建和管理只能通过连接客户端访问的资源", "resourcesSearch": "搜索资源...", "resourceAdd": "添加资源", "resourceErrorDelte": "删除资源时出错", @@ -155,9 +157,9 @@ "resourceMessageRemove": "一旦删除,资源将不再可访问。与该资源相关的所有目标也将被删除。", "resourceQuestionRemove": "您确定要从组织中删除资源吗?", "resourceHTTP": "HTTPS 资源", - "resourceHTTPDescription": "使用子域或根域名通过 HTTPS 向您的应用程序提出代理请求。", + "resourceHTTPDescription": "使用子域或基础域通过 HTTPS 请求此应用的代理请求。", "resourceRaw": "TCP/UDP 资源", - "resourceRawDescription": "使用 TCP/UDP 使用端口号向您的应用提出代理请求。", + "resourceRawDescription": "通过 TCP/UDP 使用端口号对应用程序的代理请求。只有当站点连接到节点时才能生效。", "resourceCreate": "创建资源", "resourceCreateDescription": "按照下面的步骤创建新资源", "resourceSeeAll": "查看所有资源", @@ -171,22 +173,22 @@ "noCountryFound": "找不到国家。", "siteSelectionDescription": "此站点将为目标提供连接。", "resourceType": "资源类型", - "resourceTypeDescription": "确定如何访问您的资源", + "resourceTypeDescription": "确定如何访问资源", "resourceHTTPSSettings": "HTTPS 设置", - "resourceHTTPSSettingsDescription": "配置如何通过 HTTPS 访问您的资源", + "resourceHTTPSSettingsDescription": "配置如何通过 HTTPS 访问资源", "domainType": "域类型", "subdomain": "子域名", "baseDomain": "根域名", - "subdomnainDescription": "您的资源可以访问的子域名。", + "subdomnainDescription": "可访问资源的子域。", "resourceRawSettings": "TCP/UDP 设置", - "resourceRawSettingsDescription": "配置如何通过 TCP/UDP 访问您的资源。 您映射资源到主机Pangolin服务器上的端口,这样您就可以访问服务器-公共-ip:mapped端口的资源。", + "resourceRawSettingsDescription": "配置如何通过 TCP/UDP 访问资源", "protocol": "协议", "protocolSelect": "选择协议", "resourcePortNumber": "端口号", "resourcePortNumberDescription": "代理请求的外部端口号。", "cancel": "取消", "resourceConfig": "配置片段", - "resourceConfigDescription": "复制并粘贴这些配置片段以设置您的 TCP/UDP 资源", + "resourceConfigDescription": "复制并粘贴这些配置片段以设置 TCP/UDP 资源", "resourceAddEntrypoints": "Traefik: 添加入口点", "resourceExposePorts": "Gerbil:在 Docker Compose 中显示端口", "resourceLearnRaw": "学习如何配置 TCP/UDP 资源", @@ -202,14 +204,14 @@ "proxy": "代理服务器", "internal": "内部设置", "rules": "规则", - "resourceSettingDescription": "配置您资源上的设置", + "resourceSettingDescription": "配置资源上的设置", "resourceSetting": "{resourceName} 设置", - "alwaysAllow": "一律允许", - "alwaysDeny": "一律拒绝", + "alwaysAllow": "旁路认证", + "alwaysDeny": "屏蔽访问", "passToAuth": "传递至认证", - "orgSettingsDescription": "配置您组织的一般设置", + "orgSettingsDescription": "配置组织设置", "orgGeneralSettings": "组织设置", - "orgGeneralSettingsDescription": "管理您的机构详细信息和配置", + "orgGeneralSettingsDescription": "管理机构的详细信息和配置", "saveGeneralSettings": "保存常规设置", "saveSettings": "保存设置", "orgDangerZone": "危险区域", @@ -232,7 +234,7 @@ "orgMissing": "缺少组织 ID", "orgMissingMessage": "没有组织ID,无法重新生成邀请。", "accessUsersManage": "管理用户", - "accessUsersDescription": "邀请用户并位他们添加角色以管理访问您的组织", + "accessUsersDescription": "邀请和管理访问此组织的用户", "accessUsersSearch": "搜索用户...", "accessUserCreate": "创建用户", "accessUserRemove": "删除用户", @@ -241,13 +243,13 @@ "role": "角色", "nameRequired": "名称是必填项", "accessRolesManage": "管理角色", - "accessRolesDescription": "配置角色来管理访问您的组织", + "accessRolesDescription": "创建和管理组织中用户的角色", "accessRolesSearch": "搜索角色...", "accessRolesAdd": "添加角色", "accessRoleDelete": "删除角色", "description": "描述", "inviteTitle": "打开邀请", - "inviteDescription": "管理您给其他用户的邀请", + "inviteDescription": "管理其他用户加入机构的邀请", "inviteSearch": "搜索邀请...", "minutes": "分钟", "hours": "小时", @@ -261,13 +263,13 @@ "apiKeysErrorCreate": "创建 API 密钥出错", "apiKeysErrorSetPermission": "设置权限出错", "apiKeysCreate": "生成 API 密钥", - "apiKeysCreateDescription": "为您的组织生成一个新的 API 密钥", + "apiKeysCreateDescription": "为机构生成一个新的 API 密钥", "apiKeysGeneralSettings": "权限", "apiKeysGeneralSettingsDescription": "确定此 API 密钥可以做什么", - "apiKeysList": "您的 API 密钥", - "apiKeysSave": "保存您的 API 密钥", + "apiKeysList": "新 API 密钥", + "apiKeysSave": "保存 API 密钥", "apiKeysSaveDescription": "该信息仅会显示一次,请确保将其复制到安全的位置。", - "apiKeysInfo": "您的 API 密钥是:", + "apiKeysInfo": "API 密钥是:", "apiKeysConfirmCopy": "我已复制 API 密钥", "generate": "生成", "done": "完成", @@ -424,7 +426,7 @@ "userCreated": "用户已创建", "userCreatedDescription": "用户已成功创建。", "userTypeInternal": "内部用户", - "userTypeInternalDescription": "邀请用户直接加入您的组织。", + "userTypeInternalDescription": "邀请用户直接加入组织。", "userTypeExternal": "外部用户", "userTypeExternalDescription": "创建一个具有外部身份提供商的用户。", "accessUserCreateDescription": "按照下面的步骤创建一个新用户", @@ -436,6 +438,16 @@ "inviteEmailSent": "发送邀请邮件给用户", "inviteValid": "有效", "selectDuration": "选择持续时间", + "selectResource": "选择资源", + "filterByResource": "按资源过滤", + "resetFilters": "重置过滤器", + "totalBlocked": "被Pangolin阻止的请求", + "totalRequests": "总请求", + "requestsByCountry": "请求按国家", + "requestsByDay": "按日请求", + "blocked": "已阻止", + "allowed": "允许的", + "topCountries": "顶级国家", "accessRoleSelect": "选择角色", "inviteEmailSentDescription": "一封电子邮件已经发送给用户,带有下面的访问链接。他们必须访问该链接才能接受邀请。", "inviteSentDescription": "用户已被邀请。他们必须访问下面的链接才能接受邀请。", @@ -458,13 +470,13 @@ "accessControlsSubmit": "保存访问控制", "roles": "角色", "accessUsersRoles": "管理用户和角色", - "accessUsersRolesDescription": "邀请用户并将他们添加到角色以管理访问您的组织", + "accessUsersRolesDescription": "邀请用户加入角色来管理访问组织", "key": "关键字", "createdAt": "创建于", "proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。", "proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。", "proxyEnableSSL": "启用 SSL", - "proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保您目标的 HTTPS 连接。", + "proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保目标的 HTTPS 连接。", "target": "Target", "configureTarget": "配置目标", "targetErrorFetch": "获取目标失败", @@ -480,14 +492,14 @@ "targetsErrorUpdate": "更新目标失败", "targetsErrorUpdateDescription": "更新目标时出错", "targetTlsUpdate": "TLS 设置已更新", - "targetTlsUpdateDescription": "您的 TLS 设置已成功更新", + "targetTlsUpdateDescription": "已成功更新 TLS 设置", "targetErrorTlsUpdate": "更新 TLS 设置失败", "targetErrorTlsUpdateDescription": "更新 TLS 设置时出错", "proxyUpdated": "代理设置已更新", - "proxyUpdatedDescription": "您的代理设置已成功更新", + "proxyUpdatedDescription": "已成功更新代理设置", "proxyErrorUpdate": "更新代理设置失败", "proxyErrorUpdateDescription": "更新代理设置时出错", - "targetAddr": "IP / 域名", + "targetAddr": "主机", "targetPort": "端口", "targetProtocol": "协议", "targetTlsSettings": "安全连接配置", @@ -497,12 +509,12 @@ "targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。", "targetTlsSubmit": "保存设置", "targets": "目标配置", - "targetsDescription": "设置目标来路由流量到您的后端服务", + "targetsDescription": "设置目标路由流量到后端服务", "targetStickySessions": "启用置顶会话", "targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。", "methodSelect": "选择方法", "targetSubmit": "添加目标", - "targetNoOne": "此资源没有任何目标。添加目标来配置向您后端发送请求的位置。", + "targetNoOne": "此资源没有任何目标。添加目标来配置向后端发送请求的位置。", "targetNoOneDescription": "在上面添加多个目标将启用负载平衡。", "targetsSubmit": "保存目标", "addTarget": "添加目标", @@ -516,9 +528,11 @@ "targetCreatedDescription": "目标已成功创建", "targetErrorCreate": "创建目标失败", "targetErrorCreateDescription": "创建目标时出错", + "tlsServerName": "TLS 服务器名称", + "tlsServerNameDescription": "SNI使用的 TLS 服务器名称", "save": "保存", "proxyAdditional": "附加代理设置", - "proxyAdditionalDescription": "配置你的资源如何处理代理设置", + "proxyAdditionalDescription": "配置资源如何处理代理设置", "proxyCustomHeader": "自定义主机标题", "proxyCustomHeaderDescription": "代理请求时设置的主机头。留空则使用默认值。", "proxyAdditionalSubmit": "保存代理设置", @@ -558,7 +572,7 @@ "rulesMatchType": "匹配类型", "value": "值", "rulesAbout": "关于规则", - "rulesAboutDescription": "规则使您能够依据特定条件控制资源访问权限。您可以创建基于 IP 地址或 URL 路径的规则,以允许或拒绝访问。", + "rulesAboutDescription": "规则允许您根据一组标准控制对资源的访问。 您可以创建规则允许或拒绝基于IP地址或 URL 路径的访问。", "rulesActions": "行动", "rulesActionAlwaysAllow": "总是允许:绕过所有身份验证方法", "rulesActionAlwaysDeny": "总是拒绝:阻止所有请求;无法尝试验证", @@ -570,7 +584,7 @@ "rulesEnable": "启用规则", "rulesEnableDescription": "启用或禁用此资源的规则评估", "rulesResource": "资源规则配置", - "rulesResourceDescription": "配置规则来控制对您资源的访问", + "rulesResourceDescription": "配置规则来控制对资源的访问", "ruleSubmit": "添加规则", "rulesNoOne": "没有规则。使用表单添加规则。", "rulesOrder": "规则按优先顺序评定。", @@ -586,7 +600,7 @@ "none": "无", "unknown": "未知", "resources": "资源", - "resourcesDescription": "资源是您私有网络中运行的应用程序的代理。您可以为私有网络中的任何 HTTP/HTTPS 或 TCP/UDP 服务创建资源。每个资源都必须连接到一个站点,以通过加密的 WireGuard 隧道实现私密且安全的连接。", + "resourcesDescription": "资源是在私人网络上运行的应用程序的代理。在您的私人网络上为任意HTTP/HTTPS或raw TCP/UDP服务创建资源。 每个资源必须连接到一个站点,以便通过加密的 WireGuard 隧道启用私密安全连接。", "resourcesWireGuardConnect": "采用 WireGuard 提供的加密安全连接", "resourcesMultipleAuthenticationMethods": "配置多个身份验证方法", "resourcesUsersRolesAccess": "基于用户和角色的访问控制", @@ -597,7 +611,7 @@ "resourceSelect": "选择资源", "shareLinks": "分享链接", "share": "分享链接", - "shareDescription2": "创建资源共享链接。链接提供对资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。", + "shareDescription2": "创建资源的可共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。", "shareEasyCreate": "轻松创建和分享", "shareConfigurableExpirationDuration": "可配置的过期时间", "shareSecureAndRevocable": "安全和可撤销的", @@ -607,19 +621,19 @@ "unknownCommand": "未知命令", "newtErrorFetchReleases": "无法获取版本信息: {err}", "newtErrorFetchLatest": "无法获取最新版信息: {err}", - "newtEndpoint": "Newt 端点", - "newtId": "Newt ID", - "newtSecretKey": "Newt 私钥", + "newtEndpoint": "Endpoint", + "newtId": "ID", + "newtSecretKey": "密钥", "architecture": "架构", "sites": "站点", - "siteWgAnyClients": "使用任何 WireGuard 客户端连接。您必须使用对等IP解决您的内部资源。", + "siteWgAnyClients": "使用任何 WireGuard 客户端连接。您必须使用对等IP解决内部资源问题。", "siteWgCompatibleAllClients": "与所有WireGuard客户端兼容", "siteWgManualConfigurationRequired": "需要手动配置", "userErrorNotAdminOrOwner": "用户不是管理员或所有者", "pangolinSettings": "设置 - Pangolin", "accessRoleYour": "您的角色:", "accessRoleSelect2": "选择角色", - "accessUserSelect": "选择一个用户", + "accessUserSelect": "选择用户", "otpEmailEnter": "输入电子邮件", "otpEmailEnterDescription": "在输入字段输入后按回车键添加电子邮件。", "otpEmailErrorInvalid": "无效的邮箱地址。通配符(*)必须占据整个开头部分。", @@ -671,7 +685,7 @@ "resourcePincodeSetupTitle": "设置 PIN 码", "resourcePincodeSetupTitleDescription": "设置 PIN 码来保护此资源", "resourceRoleDescription": "管理员总是可以访问此资源。", - "resourceUsersRoles": "用户和角色", + "resourceUsersRoles": "访问控制", "resourceUsersRolesDescription": "配置用户和角色可以访问此资源", "resourceUsersRolesSubmit": "保存用户和角色", "resourceWhitelistSave": "保存成功", @@ -702,6 +716,7 @@ "resourceTransferSubmit": "转移资源", "siteDestination": "目标站点", "searchSites": "搜索站点", + "countries": "国家", "accessRoleCreate": "创建角色", "accessRoleCreateDescription": "创建一个新角色来分组用户并管理他们的权限。", "accessRoleCreateSubmit": "创建角色", @@ -766,7 +781,7 @@ "idpOidcConfigure": "OAuth2/OIDC 配置", "idpOidcConfigureDescription": "配置 OAuth2/OIDC 供应商端点和凭据", "idpClientId": "客户端ID", - "idpClientIdDescription": "来自您身份提供商的 OAuth2 客户端 ID", + "idpClientIdDescription": "来自身份提供商的 OAuth2 客户端 ID", "idpClientSecret": "客户端密钥", "idpClientSecretDescription": "来自身份提供商的 OAuth2 客户端密钥", "idpAuthUrl": "授权 URL", @@ -774,7 +789,7 @@ "idpTokenUrl": "令牌 URL", "idpTokenUrlDescription": "OAuth2 令牌端点的 URL", "idpOidcConfigureAlert": "重要提示", - "idpOidcConfigureAlertDescription": "创建身份提供方后,您需要在其设置中配置回调 URL。回调 URL 会在创建成功后提供。", + "idpOidcConfigureAlertDescription": "在创建身份提供商后,您需要在身份提供商的设置中配置回调URL。 成功创建后将提供回调URL。", "idpToken": "令牌配置", "idpTokenDescription": "配置如何从 ID 令牌中提取用户信息", "idpJmespathAbout": "关于 JMESPath", @@ -826,7 +841,7 @@ "idpUpdatedDescription": "身份提供商更新成功", "redirectUrl": "重定向网址", "redirectUrlAbout": "关于重定向网址", - "redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供商设置中配置此URL。", + "redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供者的设置中配置此URL。", "pangolinAuth": "认证 - Pangolin", "verificationCodeLengthRequirements": "您的验证码必须是8个字符。", "errorOccurred": "发生错误", @@ -909,6 +924,10 @@ "passwordResetSent": "我们将发送一个验证码到这个电子邮件地址。", "passwordResetCode": "验证码", "passwordResetCodeDescription": "请检查您的电子邮件以获取验证码。", + "generatePasswordResetCode": "生成密码重置代码", + "passwordResetCodeGenerated": "密码重置代码已生成", + "passwordResetCodeGeneratedDescription": "与用户分享此代码。他们可以用它来重置他们的密码。", + "passwordResetUrl": "Reset URL", "passwordNew": "新密码", "passwordNewConfirm": "确认新密码", "changePassword": "更改密码", @@ -926,6 +945,9 @@ "pincodeAuth": "验证器代码", "pincodeSubmit2": "提交代码", "passwordResetSubmit": "请求重置", + "passwordResetAlreadyHaveCode": "输入密码重置码", + "passwordResetSmtpRequired": "请联系您的管理员", + "passwordResetSmtpRequiredDescription": "需要密码重置密码。请联系您的管理员寻求帮助。", "passwordBack": "回到密码", "loginBack": "返回登录", "signup": "注册", @@ -1091,12 +1113,15 @@ "actionListSiteResources": "列出站点资源", "actionUpdateSiteResource": "更新站点资源", "actionListInvitations": "邀请列表", + "actionExportLogs": "导出日志", + "actionViewLogs": "查看日志", "noneSelected": "未选择", "orgNotFound2": "未找到组织。", "searchProgress": "搜索中...", "create": "创建", "orgs": "组织", "loginError": "登录时出错", + "loginRequiredForDevice": "需要登录才能验证您的设备。", "passwordForgot": "忘记密码?", "otpAuth": "两步验证", "otpAuthDescription": "从您的身份验证程序中输入代码或您的单次备份代码。", @@ -1151,19 +1176,29 @@ "sidebarHome": "首页", "sidebarSites": "站点", "sidebarResources": "资源", + "sidebarProxyResources": "公开的", + "sidebarClientResources": "非公开的", "sidebarAccessControl": "访问控制", + "sidebarLogsAndAnalytics": "日志与分析", "sidebarUsers": "用户", + "sidebarAdmin": "管理员", "sidebarInvitations": "邀请", "sidebarRoles": "角色", - "sidebarShareableLinks": "分享链接", + "sidebarShareableLinks": "链接", "sidebarApiKeys": "API密钥", "sidebarSettings": "设置", "sidebarAllUsers": "所有用户", "sidebarIdentityProviders": "身份提供商", "sidebarLicense": "证书", "sidebarClients": "客户端", + "sidebarUserDevices": "用户", + "sidebarMachineClients": "机", "sidebarDomains": "域", + "sidebarGeneral": "概览", + "sidebarLogAndAnalytics": "日志与分析", "sidebarBluePrints": "蓝图", + "sidebarOrganization": "组织", + "sidebarLogsAnalytics": "分析", "blueprints": "蓝图", "blueprintsDescription": "应用声明配置并查看先前运行的", "blueprintAdd": "添加蓝图", @@ -1174,7 +1209,7 @@ "blueprintDetailsDescription": "查看应用蓝图的结果和发生的任何错误", "blueprintInfo": "蓝图信息", "message": "留言", - "blueprintContentsDescription": "定义描述您基础设施的 YAML 内容", + "blueprintContentsDescription": "定义描述基础设施的 YAML 内容", "blueprintErrorCreateDescription": "应用蓝图时出错", "blueprintErrorCreate": "创建蓝图时出错", "searchBlueprintProgress": "搜索蓝图...", @@ -1230,15 +1265,15 @@ "loading": "加载中", "restart": "重启", "domains": "域", - "domainsDescription": "管理您的组织域", + "domainsDescription": "创建和管理组织中可用的域", "domainsSearch": "搜索域...", "domainAdd": "添加域", - "domainAddDescription": "在您的组织中注册新域", + "domainAddDescription": "注册一个新域名到组织", "domainCreate": "创建域", "domainCreatedDescription": "域创建成功", "domainDeletedDescription": "成功删除域", - "domainQuestionRemove": "您确定要从您的帐户中删除域名吗?", - "domainMessageRemove": "移除后,该域将不再与您的账户关联。", + "domainQuestionRemove": "您确定要删除域名吗?", + "domainMessageRemove": "一旦删除,域将不再与组织相关联。", "domainConfirmDelete": "确认删除域", "domainDelete": "删除域", "domain": "域", @@ -1257,7 +1292,7 @@ "pending": "待定", "sidebarBilling": "计费", "billing": "计费", - "orgBillingDescription": "管理您的账单信息和订阅", + "orgBillingDescription": "管理账单信息和订阅", "github": "GitHub", "pangolinHosted": "Pangolin 托管", "fossorial": "Fossorial", @@ -1285,9 +1320,9 @@ "productUpdateTitle": "产品更新", "productUpdateEmpty": "无更新", "dismissAll": "关闭所有", - "pangolinUpdateAvailable": "新版本可用", + "pangolinUpdateAvailable": "可用更新", "pangolinUpdateAvailableInfo": "版本 {version} 已准备就绪", - "pangolinUpdateAvailableReleaseNotes": "查看发布笔记", + "pangolinUpdateAvailableReleaseNotes": "查看发布说明", "newtUpdateAvailable": "更新可用", "newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。", "domainPickerEnterDomain": "域名", @@ -1300,7 +1335,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "检查可用性...", - "domainPickerNoMatchingDomains": "未找到匹配的域名。尝试不同的域名或检查您组织的域名设置。", + "domainPickerNoMatchingDomains": "未找到匹配的域。请尝试不同的域或检查组织的域设置。", "domainPickerOrganizationDomains": "组织域", "domainPickerProvidedDomains": "提供的域", "domainPickerSubdomain": "子域:{subdomain}", @@ -1334,7 +1369,7 @@ "billingModifySubscription": "修改订阅", "billingStartSubscription": "开始订阅", "billingRecurringCharge": "周期性收费", - "billingManageSubscriptionSettings": "管理您的订阅设置和偏好", + "billingManageSubscriptionSettings": "管理订阅设置和首选项", "billingNoActiveSubscription": "您没有活跃的订阅。开始订阅以增加使用限制。", "billingFailedToLoadSubscription": "无法加载订阅", "billingFailedToLoadUsage": "无法加载使用情况", @@ -1345,9 +1380,9 @@ "billingPortalError": "门户错误", "billingDataUsageInfo": "当连接到云端时,您将为通过安全隧道传输的所有数据收取费用。 这包括您所有站点的进出流量。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取数据。", "billingOnlineTimeInfo": "您要根据您的网站连接到云端的时间长短收取费用。 例如,44,640分钟等于一个24/7全月运行的网站。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取费用。", - "billingUsersInfo": "根据您组织中的活跃用户数量收费。按日计算账单。", - "billingDomainInfo": "根据组织中活跃域的数量收费。按日计算账单。", - "billingRemoteExitNodesInfo": "根据您组织中已管理节点的数量收费。按日计算账单。", + "billingUsersInfo": "您为组织中的每个用户收取费用。每日计费是根据您组织中活跃用户帐户的数量计算的。", + "billingDomainInfo": "您在组织中的每个域都要收取费用。每日计费是根据您组织中的活动域帐户数计算的。", + "billingRemoteExitNodesInfo": "您为组织中的每个管理节点收取费用。计费是每日根据您组织中活跃的管理节点数计算的。", "domainNotFound": "域未找到", "domainNotFoundDescription": "此资源已禁用,因为该域不再在我们的系统中存在。请为此资源设置一个新域。", "failed": "失败", @@ -1430,29 +1465,32 @@ "and": "和", "privacyPolicy": "隐私政策" }, + "signUpMarketing": { + "keepMeInTheLoop": "通过电子邮件让我在循环中保持新闻、更新和新功能。" + }, "siteRequired": "需要站点。", "olmTunnel": "Olm 隧道", "olmTunnelDescription": "使用 Olm 进行客户端连接", "errorCreatingClient": "创建客户端出错", "clientDefaultsNotFound": "未找到客户端默认值", "createClient": "创建客户端", - "createClientDescription": "创建一个新客户端来连接您的站点", + "createClientDescription": "创建一个新客户端来访问私有资源", "seeAllClients": "查看所有客户端", "clientInformation": "客户端信息", "clientNamePlaceholder": "客户端名称", "address": "地址", "subnetPlaceholder": "子网", - "addressDescription": "此客户端将用于连接的地址", + "addressDescription": "客户的内部地址。必须属于组织的子网。", "selectSites": "选择站点", "sitesDescription": "客户端将与所选站点进行连接", "clientInstallOlm": "安装 Olm", "clientInstallOlmDescription": "在您的系统上运行 Olm", - "clientOlmCredentials": "Olm 凭据", - "clientOlmCredentialsDescription": "这是 Olm 服务器的身份验证方式", - "olmEndpoint": "Olm 端点", - "olmId": "Olm ID", - "olmSecretKey": "Olm 私钥", - "clientCredentialsSave": "保存您的凭据", + "clientOlmCredentials": "全权证书", + "clientOlmCredentialsDescription": "这是客户端如何通过服务器进行身份验证", + "olmEndpoint": "Endpoint", + "olmId": "ID", + "olmSecretKey": "密钥", + "clientCredentialsSave": "保存证书", "clientCredentialsSaveDescription": "该信息仅会显示一次,请确保将其复制到安全位置。", "generalSettingsDescription": "配置此客户端的常规设置", "clientUpdated": "客户端已更新", @@ -1463,9 +1501,7 @@ "sitesFetchError": "获取站点时出错。", "olmErrorFetchReleases": "获取 Olm 发布版本时出错。", "olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。", - "remoteSubnets": "远程子网", "enterCidrRange": "输入 CIDR 范围", - "remoteSubnetsDescription": "添加可以通过客户端远程访问该站点的CIDR范围。使用类似10.0.0.0/24的格式。这仅适用于VPN客户端连接。", "resourceEnableProxy": "启用公共代理", "resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。", "externalProxyEnabled": "外部代理已启用", @@ -1483,14 +1519,15 @@ "enableHealthChecksDescription": "监视此目标的健康状况。如果需要,您可以监视一个不同的终点。", "healthScheme": "方法", "healthSelectScheme": "选择方法", + "healthCheckPortInvalid": "健康检查端口必须介于 1 到 65535 之间", "healthCheckPath": "路径", "healthHostname": "IP / 主机", "healthPort": "端口", "healthCheckPathDescription": "用于检查健康状态的路径。", - "healthyIntervalSeconds": "正常间隔", - "unhealthyIntervalSeconds": "不正常间隔", + "healthyIntervalSeconds": "健康间隔(秒)", + "unhealthyIntervalSeconds": "不健康间隔 (秒)", "IntervalSeconds": "正常间隔", - "timeoutSeconds": "超时", + "timeoutSeconds": "超时(秒)", "timeIsInSeconds": "时间以秒为单位", "retryAttempts": "重试次数", "expectedResponseCodes": "期望响应代码", @@ -1526,12 +1563,12 @@ "resourceEditDomain": "编辑域名", "siteName": "站点名称", "proxyPort": "端口", - "resourcesTableProxyResources": "代理资源", - "resourcesTableClientResources": "客户端资源", + "resourcesTableProxyResources": "公开的", + "resourcesTableClientResources": "非公开的", "resourcesTableNoProxyResourcesFound": "未找到代理资源。", "resourcesTableNoInternalResourcesFound": "未找到内部资源。", "resourcesTableDestination": "目标", - "resourcesTableTheseResourcesForUseWith": "这些资源供...使用", + "resourcesTableAlias": "Alias", "resourcesTableClients": "客户端", "resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。", "resourcesTableNoTargets": "没有目标", @@ -1540,8 +1577,8 @@ "resourcesTableOffline": "离线的", "resourcesTableUnknown": "未知的", "resourcesTableNotMonitored": "未监视的", - "editInternalResourceDialogEditClientResource": "编辑客户端资源", - "editInternalResourceDialogUpdateResourceProperties": "更新{resourceName}的资源属性和目标配置。", + "editInternalResourceDialogEditClientResource": "编辑私有资源", + "editInternalResourceDialogUpdateResourceProperties": "更新{resourceName}的资源配置和访问控制。", "editInternalResourceDialogResourceProperties": "资源属性", "editInternalResourceDialogName": "名称", "editInternalResourceDialogProtocol": "协议", @@ -1560,11 +1597,22 @@ "editInternalResourceDialogInvalidIPAddressFormat": "无效的IP地址格式", "editInternalResourceDialogDestinationPortMin": "目标端口必须至少为1", "editInternalResourceDialogDestinationPortMax": "目标端口必须小于65536", + "editInternalResourceDialogPortModeRequired": "端口模式需要协议、代理端口和目的端口", + "editInternalResourceDialogMode": "模式", + "editInternalResourceDialogModePort": "端口", + "editInternalResourceDialogModeHost": "主机", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "目标", + "editInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。", + "editInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP或主机名地址。", + "editInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。", + "editInternalResourceDialogAlias": "Alias", + "editInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。", "createInternalResourceDialogNoSitesAvailable": "暂无可用站点", "createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一个子网的Newt站点来创建内部资源。", "createInternalResourceDialogClose": "关闭", - "createInternalResourceDialogCreateClientResource": "创建客户端资源", - "createInternalResourceDialogCreateClientResourceDescription": "创建一个新资源,该资源将可供连接到所选站点的客户端访问。", + "createInternalResourceDialogCreateClientResource": "创建私有资源", + "createInternalResourceDialogCreateClientResourceDescription": "创建一个新资源只能为连接到组织的客户端访问", "createInternalResourceDialogResourceProperties": "资源属性", "createInternalResourceDialogName": "名称", "createInternalResourceDialogSite": "站点", @@ -1593,11 +1641,22 @@ "createInternalResourceDialogInvalidIPAddressFormat": "无效的IP地址格式", "createInternalResourceDialogDestinationPortMin": "目标端口必须至少为1", "createInternalResourceDialogDestinationPortMax": "目标端口必须小于65536", + "createInternalResourceDialogPortModeRequired": "端口模式需要协议、代理端口和目的端口", + "createInternalResourceDialogMode": "模式", + "createInternalResourceDialogModePort": "端口", + "createInternalResourceDialogModeHost": "主机", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "目标", + "createInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。", + "createInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。", + "createInternalResourceDialogAlias": "Alias", + "createInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。", "siteConfiguration": "配置", "siteAcceptClientConnections": "接受客户端连接", - "siteAcceptClientConnectionsDescription": "允许其他设备通过此Newt实例使用客户端作为网关连接。", - "siteAddress": "站点地址", - "siteAddressDescription": "指定主机的IP地址以供客户端连接。这是Pangolin网络中站点的内部地址,供客户端访问。必须在Org子网内。", + "siteAcceptClientConnectionsDescription": "允许用户设备和客户端访问此站点上的资源。这可以稍后更改。", + "siteAddress": "站点地址 (高级)", + "siteAddressDescription": "站点的内部地址。必须属于组织的子网。", + "siteNameDescription": "可以稍后更改的站点显示名称。", "autoLoginExternalIdp": "自动使用外部IDP登录", "autoLoginExternalIdpDescription": "立即将用户重定向到外部IDP进行身份验证。", "selectIdp": "选择IDP", @@ -1611,7 +1670,7 @@ "autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。", "autoLoginErrorGeneratingUrl": "生成身份验证URL失败。", "remoteExitNodeManageRemoteExitNodes": "远程节点", - "remoteExitNodeDescription": "自我主机一个或多个远程节点来扩展您的网络连接并减少对云的依赖性", + "remoteExitNodeDescription": "自我主机一个或多个远程节点来扩展网络连接并减少对云的依赖性", "remoteExitNodes": "节点", "searchRemoteExitNodes": "搜索节点...", "remoteExitNodeAdd": "添加节点", @@ -1623,11 +1682,11 @@ "sidebarRemoteExitNodes": "远程节点", "remoteExitNodeCreate": { "title": "创建节点", - "description": "创建一个新节点来扩展您的网络连接", + "description": "创建一个新节点来扩展网络连接", "viewAllButton": "查看所有节点", "strategy": { "title": "创建策略", - "description": "选择此选项以手动配置您的节点或生成新凭据。", + "description": "选择此选项以手动配置节点或生成新凭据。", "adopt": { "title": "采纳节点", "description": "如果您已经拥有该节点的凭据,请选择此项。" @@ -1648,7 +1707,7 @@ }, "generate": { "title": "生成的凭据", - "description": "使用这些生成的凭据来配置您的节点", + "description": "使用这些生成的凭据来配置节点", "nodeIdTitle": "节点 ID", "secretTitle": "密钥", "saveCredentialsTitle": "将凭据添加到配置中", @@ -1724,16 +1783,16 @@ "idpTypeLabel": "身份提供者类型", "roleMappingExpressionPlaceholder": "例如: contains(group, 'admin' &'Admin' || 'Member'", "idpGoogleConfiguration": "Google 配置", - "idpGoogleConfigurationDescription": "配置您的 Google OAuth2 凭据", - "idpGoogleClientIdDescription": "您的 Google OAuth2 客户端 ID", - "idpGoogleClientSecretDescription": "您的 Google OAuth2 客户端密钥", + "idpGoogleConfigurationDescription": "配置 Google OAuth2 凭据", + "idpGoogleClientIdDescription": "Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Google OAuth2 客户端密钥", "idpAzureConfiguration": "Azure Entra ID 配置", - "idpAzureConfigurationDescription": "配置您的 Azure Entra ID OAuth2 凭据", + "idpAzureConfigurationDescription": "配置 Azure Entra ID OAuth2 凭据", "idpTenantId": "租户 ID", - "idpTenantIdPlaceholder": "您的租户ID", - "idpAzureTenantIdDescription": "您的 Azure 租户ID (在 Azure Active Directory 概览中发现)", - "idpAzureClientIdDescription": "您的 Azure 应用程序注册客户端 ID", - "idpAzureClientSecretDescription": "您的 Azure 应用程序注册客户端密钥", + "idpTenantIdPlaceholder": "tenant-id", + "idpAzureTenantIdDescription": "Azure 租户ID (在 Azure Active Directory 概览中找到)", + "idpAzureClientIdDescription": "Azure 应用注册客户端 ID", + "idpAzureClientSecretDescription": "Azure 应用程序注册客户端密钥", "idpGoogleTitle": "谷歌", "idpGoogleAlt": "Google", "idpAzureTitle": "Azure Entra ID", @@ -1741,14 +1800,14 @@ "idpGoogleConfigurationTitle": "Google 配置", "idpAzureConfigurationTitle": "Azure Entra ID 配置", "idpTenantIdLabel": "租户 ID", - "idpAzureClientIdDescription2": "您的 Azure 应用程序注册客户端 ID", - "idpAzureClientSecretDescription2": "您的 Azure 应用程序注册客户端密钥", + "idpAzureClientIdDescription2": "Azure 应用注册客户端 ID", + "idpAzureClientSecretDescription2": "Azure 应用程序注册客户端密钥", "idpGoogleDescription": "Google OAuth2/OIDC 提供商", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "子网", "subnetDescription": "此组织网络配置的子网。", "authPage": "认证页面", - "authPageDescription": "配置您的组织认证页面", + "authPageDescription": "配置组织认证页面", "authPageDomain": "认证页面域", "noDomainSet": "没有域设置", "changeDomain": "更改域", @@ -1758,7 +1817,7 @@ "setAuthPageDomain": "设置认证页面域", "failedToFetchCertificate": "获取证书失败", "failedToRestartCertificate": "重新启动证书失败", - "addDomainToEnableCustomAuthPages": "为您的组织添加域名以启用自定义认证页面", + "addDomainToEnableCustomAuthPages": "添加域名以启用组织自定义认证页面", "selectDomainForOrgAuthPage": "选择组织认证页面的域", "domainPickerProvidedDomain": "提供的域", "domainPickerFreeProvidedDomain": "免费提供的域", @@ -1773,7 +1832,7 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" 无法为 {domain} 变为有效。", "domainPickerSubdomainSanitized": "子域已净化", "domainPickerSubdomainCorrected": "\"{sub}\" 已被更正为 \"{sanitized}\"", - "orgAuthSignInTitle": "登录到您的组织", + "orgAuthSignInTitle": "登录到组织", "orgAuthChooseIdpDescription": "选择您的身份提供商以继续", "orgAuthNoIdpConfigured": "此机构没有配置任何身份提供者。您可以使用您的 Pangolin 身份登录。", "orgAuthSignInWithPangolin": "使用 Pangolin 登录", @@ -1791,7 +1850,7 @@ "enableTwoFactorAuthentication": "启用两步验证", "completeSecuritySteps": "完成安全步骤", "securitySettings": "安全设置", - "securitySettingsDescription": "配置您组织的安全策略", + "securitySettingsDescription": "配置组织安全策略", "requireTwoFactorForAllUsers": "所有用户需要两步验证", "requireTwoFactorDescription": "如果启用,此组织的所有内部用户必须启用双重身份验证才能访问组织。", "requireTwoFactorDisabledDescription": "此功能需要有效的许可证(企业)或活动订阅(SaS)", @@ -1854,8 +1913,12 @@ "enterpriseEdition": "企业版", "unlicensed": "未授权", "beta": "测试版", - "manageClients": "管理客户端", - "manageClientsDescription": "客户端是可以连接到您的站点的设备", + "manageUserDevices": "用户设备", + "manageUserDevicesDescription": "查看和管理用户用来私下连接到资源的设备", + "manageMachineClients": "管理机器客户端", + "manageMachineClientsDescription": "创建和管理服务器和系统用于私密连接到资源的客户端", + "clientsTableUserClients": "用户", + "clientsTableMachineClients": "机", "licenseTableValidUntil": "有效期至", "saasLicenseKeysSettingsTitle": "企业许可证", "saasLicenseKeysSettingsDescription": "为自我托管的 Pangolin 实例生成和管理企业许可证密钥", @@ -1990,11 +2053,12 @@ "pathRewriteStripLabel": "条形图", "sidebarEnableEnterpriseLicense": "启用企业许可证", "cannotbeUndone": "无法撤消。", - "toConfirm": "确认", + "toConfirm": "确认.", "deleteClientQuestion": "您确定要从站点和组织中删除客户吗?", "clientMessageRemove": "一旦删除,客户端将无法连接到站点。", "sidebarLogs": "日志", "request": "请求", + "requests": "请求", "logs": "日志", "logsSettingsDescription": "监视从此orginization中收集的日志", "searchLogs": "搜索日志...", @@ -2020,6 +2084,7 @@ "ip": "IP", "reason": "原因", "requestLogs": "请求日志", + "requestAnalytics": "请求分析", "host": "主机", "location": "地点", "actionLogs": "操作日志", @@ -2029,6 +2094,7 @@ "logRetention": "日志保留", "logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志", "requestLogsDescription": "查看此机构资源的详细请求日志", + "requestAnalyticsDescription": "查看此机构资源的详细请求分析", "logRetentionRequestLabel": "请求日志保留", "logRetentionRequestDescription": "保留请求日志的时间", "logRetentionAccessLabel": "访问日志保留", @@ -2042,6 +2108,7 @@ "logRetention30Days": "30 天", "logRetention90Days": "90 天", "logRetentionForever": "永远的", + "logRetentionEndOfFollowingYear": "下一年结束", "actionLogsDescription": "查看此机构执行的操作历史", "accessLogsDescription": "查看此机构资源的访问认证请求", "licenseRequiredToUse": "需要企业许可证才能使用此功能。", @@ -2052,7 +2119,7 @@ "preferWildcardCert": "喜欢通配符证书", "unverified": "未验证", "domainSetting": "域设置", - "domainSettingDescription": "配置您的域的设置", + "domainSettingDescription": "配置域设置", "preferWildcardCertDescription": "尝试生成通配符证书(需要正确配置的证书解析器)。", "recordName": "记录名称", "auto": "自动操作", @@ -2066,15 +2133,15 @@ "olmUpdateAvailableInfo": "有最新版本的 Olm 可用。请更新到最新版本以获取最佳体验。", "client": "客户端:", "proxyProtocol": "代理协议设置", - "proxyProtocolDescription": "配置代理协议以保留TCP/UDP 服务的客户端IP地址。", + "proxyProtocolDescription": "配置代理协议以保留TCP服务的客户端 IP 地址。", "enableProxyProtocol": "启用代理协议", - "proxyProtocolInfo": "为TCP/UDP 后端保留客户端IP地址", + "proxyProtocolInfo": "为TCP后端保留客户端IP地址", "proxyProtocolVersion": "代理协议版本", "version1": " 版本 1 (推荐)", "version2": "版本 2", "versionDescription": "版本 1 是基于文本和广泛支持的版本。版本 2 是二进制和更有效率但不那么兼容。", "warning": "警告", - "proxyProtocolWarning": "您的后端应用程序必须配置为接受代理协议连接。如果您的后端不支持代理协议,启用这将会中断所有连接。 请务必从Traefik配置您的后端到信任代理协议标题。", + "proxyProtocolWarning": "后端应用程序必须配置为接受代理协议连接。 如果您的后端不支持代理协议,启用此功能将会中断所有连接,只有当您知道自己在做什么时才能启用此功能。 请务必从Traefik配置您的后端到信任代理协议标题。", "restarting": "正在重启...", "manual": "手动模式", "messageSupport": "消息支持", @@ -2097,6 +2164,43 @@ "supportMessageSent": "消息已发送!", "supportWillContact": "我们很快就会联系起来!", "selectLogRetention": "选择保留日志", + "terms": "条款", + "privacy": "隐私", + "security": "安全", + "docs": "文档", + "deviceActivation": "设备激活", + "deviceCodeInvalidFormat": "代码必须是9个字符(如A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "无效或过期的代码", + "deviceCodeVerifyFailed": "验证设备代码失败", + "signedInAs": "登录为", + "deviceCodeEnterPrompt": "输入设备上显示的代码", + "continue": "继续", + "deviceUnknownLocation": "未知位置", + "deviceAuthorizationRequested": "此授权请求来自{location},日期为{date}。请确保您信任此设备,因为它将获得帐户访问权限。", + "deviceLabel": "设备: {deviceName}", + "deviceWantsAccess": "想要访问您的帐户", + "deviceExistingAccess": "现有访问权限:", + "deviceFullAccess": "完全访问您的帐户", + "deviceOrganizationsAccess": "访问您的帐户拥有访问权限的所有组织", + "deviceAuthorize": "授权{applicationName}", + "deviceConnected": "设备已连接!", + "deviceAuthorizedMessage": "设备被授权访问您的帐户。", + "pangolinCloud": "邦戈林云", + "viewDevices": "查看设备", + "viewDevicesDescription": "管理您已连接的设备", + "noDevices": "未找到设备", + "dateCreated": "创建日期", + "unnamedDevice": "未命名设备", + "deviceQuestionRemove": "您确定要删除此设备吗?", + "deviceMessageRemove": "此操作不能撤消。", + "deviceDeleteConfirm": "删除设备", + "deleteDevice": "删除设备", + "errorLoadingDevices": "加载设备时出错", + "failedToLoadDevices": "加载设备失败", + "deviceDeleted": "设备已删除", + "deviceDeletedDescription": "设备已成功删除。", + "errorDeletingDevice": "删除设备时出错", + "failedToDeleteDevice": "删除设备失败", "showColumns": "显示列", "hideColumns": "隐藏列", "columnVisibility": "列可见性", @@ -2111,10 +2215,14 @@ "enableSelected": "启用选中的", "disableSelected": "禁用选中的", "checkSelectedStatus": "检查选中的状态", + "clients": "客户端", + "accessClientSelect": "选择机器客户端", + "resourceClientDescription": "机器客户端可以访问此资源", + "regenerate": "重新生成", "credentials": "全权证书", "savecredentials": "保存证书", - "regeneratecredentials": "重置键", - "regenerateCredentials": "重新生成和保存您的凭据", + "regenerateCredentialsButton": "重新生成证书", + "regenerateCredentials": "重新生成证书", "generatedcredentials": "生成的证书", "copyandsavethesecredentials": "复制和保存这些凭据", "copyandsavethesecredentialsdescription": "这些凭据将不会在您离开此页面后再显示。现在安全地保存。", @@ -2122,13 +2230,12 @@ "credentialsSavedDescription": "已成功生成和保存凭据。", "credentialsSaveError": "证书保存错误", "credentialsSaveErrorDescription": "更新和保存凭据时出错。", - "regenerateCredentialsWarning": "重新生成凭据将使以前的凭据失效。请确保更新使用这些凭据的任何配置。", + "regenerateCredentialsWarning": "重新生成凭据将使以前的凭据失效并导致断开连接。请确保更新使用这些凭据的任何配置。", "confirm": "确认", "regenerateCredentialsConfirmation": "您确定要重新生成凭据吗?", "endpoint": "Endpoint", "Id": "Id", "SecretKey": "秘密密钥", - "featureDisabledTooltip": "此功能仅在企业计划中可用,需要许可证才能使用。", "niceId": "好的 ID", "niceIdUpdated": "好的 ID 已更新", "niceIdUpdatedSuccessfully": "Nice ID 更新成功", @@ -2136,5 +2243,32 @@ "niceIdUpdateErrorDescription": "更新Nice ID时出错。", "niceIdCannotBeEmpty": "好的 ID 不能为空", "enterIdentifier": "输入标识符", - "identifier": "Identifier" + "identifier": "Identifier", + "deviceLoginUseDifferentAccount": "不是你?使用一个不同的帐户。", + "deviceLoginDeviceRequestingAccessToAccount": "设备正在请求访问此帐户。", + "noData": "无数据", + "machineClients": "机器客户端", + "install": "安装", + "run": "运行", + "clientNameDescription": "可以稍后更改的客户端的显示名称。", + "clientAddress": "客户端地址 (高级)", + "setupFailedToFetchSubnet": "获取默认子网失败", + "setupSubnetAdvanced": "子网 (高级)", + "setupSubnetDescription": "该组织内部网络的子网。", + "siteRegenerateAndDisconnect": "重新生成和断开", + "siteRegenerateAndDisconnectConfirmation": "您确定要重新生成凭据并断开此站点连接吗?", + "siteRegenerateAndDisconnectWarning": "这将重新生成凭据并立即断开站点。该站点将需要重新启动新凭据。", + "siteRegenerateCredentialsConfirmation": "您确定要重新生成此站点的凭据吗?", + "siteRegenerateCredentialsWarning": "这将重新生成凭据。站点将保持连接,直到您手动重启并使用新凭据。", + "clientRegenerateAndDisconnect": "重新生成和断开", + "clientRegenerateAndDisconnectConfirmation": "您确定要重新生成凭据并断开此客户端连接吗?", + "clientRegenerateAndDisconnectWarning": "这将重新生成凭据并立即断开客户端。客户端需要重新启动新凭据。", + "clientRegenerateCredentialsConfirmation": "您确定要重新生成此客户端的凭据吗?", + "clientRegenerateCredentialsWarning": "这将重新生成凭据。客户端将保持连接,直到您手动重启它并使用新凭据。", + "remoteExitNodeRegenerateAndDisconnect": "重新生成和断开", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "您确定要重新生成凭据并断开此远程退出节点?", + "remoteExitNodeRegenerateAndDisconnectWarning": "这将重新生成凭据并立即断开远程退出节点。远程退出节点将需要用新的凭据重启。", + "remoteExitNodeRegenerateCredentialsConfirmation": "您确定要重新生成此远程退出节点的凭据吗?", + "remoteExitNodeRegenerateCredentialsWarning": "这将重新生成凭据。远程退出节点将保持连接,直到您手动重启它并使用新凭据。", + "agent": "代理" } diff --git a/package-lock.json b/package-lock.json index 919730b1..b594dea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { - "@asteasolutions/zod-to-openapi": "8.1.0", - "@aws-sdk/client-s3": "3.922.0", - "@faker-js/faker": "^10.1.0", - "@headlessui/react": "^2.2.9", + "@asteasolutions/zod-to-openapi": "8.2.0", + "@aws-sdk/client-s3": "3.947.0", + "@faker-js/faker": "10.1.0", + "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", - "@monaco-editor/react": "^4.7.0", - "@node-rs/argon2": "^2.0.2", + "@monaco-editor/react": "4.7.0", + "@node-rs/argon2": "2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@radix-ui/react-avatar": "1.1.11", @@ -26,133 +26,133 @@ "@radix-ui/react-icons": "1.3.2", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-progress": "1.1.8", "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.8", "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-tooltip": "^1.2.8", - "@react-email/components": "0.5.7", - "@react-email/render": "^1.3.2", - "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.2.2", - "@simplewebauthn/server": "^13.2.2", - "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.90.6", + "@radix-ui/react-tooltip": "1.2.8", + "@react-email/components": "1.0.1", + "@react-email/render": "2.0.0", + "@react-email/tailwind": "2.0.1", + "@simplewebauthn/browser": "13.2.2", + "@simplewebauthn/server": "13.2.2", + "@tailwindcss/forms": "0.5.10", + "@tanstack/react-query": "5.90.12", "@tanstack/react-table": "8.21.3", - "arctic": "^3.7.0", - "axios": "^1.13.2", - "better-sqlite3": "11.7.0", + "arctic": "3.7.0", + "axios": "1.13.2", + "better-sqlite3": "11.9.1", "canvas-confetti": "1.9.4", - "class-variance-authority": "^0.7.1", + "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", - "cookie": "^1.0.2", + "cookie": "1.1.1", "cookie-parser": "1.4.7", - "cookies": "^0.9.1", + "cookies": "0.9.1", "cors": "2.8.5", - "crypto-js": "^4.2.0", - "d3": "^7.9.0", + "crypto-js": "4.2.0", + "d3": "7.9.0", "date-fns": "4.1.0", - "drizzle-orm": "0.44.7", + "drizzle-orm": "0.45.0", "eslint": "9.39.1", - "eslint-config-next": "16.0.3", - "express": "5.1.0", + "eslint-config-next": "16.0.8", + "express": "5.2.1", "express-rate-limit": "8.2.1", - "glob": "11.1.0", + "glob": "13.0.0", "helmet": "8.1.0", - "http-errors": "2.0.0", - "i": "^0.3.7", + "http-errors": "2.0.1", + "i": "0.3.7", "input-otp": "1.4.2", "ioredis": "5.8.2", - "jmespath": "^0.16.0", + "jmespath": "0.16.0", "js-yaml": "4.1.1", - "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.552.0", + "jsonwebtoken": "9.0.3", + "lucide-react": "0.556.0", "maxmind": "5.0.1", "moment": "2.30.1", "next": "15.5.7", - "next-intl": "^4.4.0", + "next-intl": "4.5.8", "next-themes": "0.4.6", - "nextjs-toploader": "^3.9.17", + "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.10", - "npm": "^11.6.4", - "nprogress": "^0.2.0", + "nodemailer": "7.0.11", + "npm": "11.6.4", + "nprogress": "0.2.0", "oslo": "1.2.1", - "pg": "^8.16.2", - "posthog-node": "^5.11.2", + "pg": "8.16.3", + "posthog-node": "5.17.2", "qrcode.react": "4.2.0", "react": "19.2.1", - "react-day-picker": "9.11.1", + "react-day-picker": "9.12.0", "react-dom": "19.2.1", - "react-easy-sort": "^1.8.0", - "react-hook-form": "7.66.0", - "react-icons": "^5.5.0", + "react-easy-sort": "1.8.0", + "react-hook-form": "7.68.0", + "react-icons": "5.5.0", "rebuild": "0.1.2", - "recharts": "^2.15.4", - "reodotdev": "^1.0.0", - "resend": "^6.4.2", - "semver": "^7.7.3", - "stripe": "18.2.1", - "swagger-ui-express": "^5.0.1", - "tailwind-merge": "3.3.1", - "topojson-client": "^3.1.0", - "tw-animate-css": "^1.3.8", - "uuid": "^13.0.0", + "recharts": "2.15.4", + "reodotdev": "1.0.0", + "resend": "6.5.2", + "semver": "7.7.3", + "stripe": "20.0.0", + "swagger-ui-express": "5.0.1", + "tailwind-merge": "3.4.0", + "topojson-client": "3.1.0", + "tw-animate-css": "1.4.0", + "uuid": "13.0.0", "vaul": "1.1.2", - "visionscarto-world-atlas": "^1.0.0", - "winston": "3.18.3", + "visionscarto-world-atlas": "1.0.0", + "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", "ws": "8.18.3", - "yaml": "^2.8.1", + "yaml": "2.8.2", "yargs": "18.0.0", - "zod": "4.1.12", + "zod": "4.1.13", "zod-validation-error": "5.0.0" }, "devDependencies": { "@dotenvx/dotenvx": "1.51.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "4.3.2", - "@tailwindcss/postcss": "^4.1.17", - "@tanstack/react-query-devtools": "^5.90.2", - "@types/better-sqlite3": "7.6.12", + "@tailwindcss/postcss": "4.1.17", + "@tanstack/react-query-devtools": "5.91.1", + "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", - "@types/crypto-js": "^4.2.2", - "@types/d3": "^7.4.3", - "@types/express": "5.0.5", - "@types/express-session": "^1.18.2", - "@types/jmespath": "^0.15.2", + "@types/crypto-js": "4.2.2", + "@types/d3": "7.4.3", + "@types/express": "5.0.6", + "@types/express-session": "1.18.2", + "@types/jmespath": "0.15.2", "@types/js-yaml": "4.0.9", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "24.10.1", - "@types/nodemailer": "7.0.3", - "@types/nprogress": "^0.2.3", + "@types/jsonwebtoken": "9.0.10", + "@types/node": "24.10.2", + "@types/nodemailer": "7.0.4", + "@types/nprogress": "0.2.3", "@types/pg": "8.15.6", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", - "@types/semver": "^7.7.1", - "@types/swagger-ui-express": "^4.1.8", - "@types/topojson-client": "^3.1.5", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@types/semver": "7.7.1", + "@types/swagger-ui-express": "4.1.8", + "@types/topojson-client": "3.1.5", "@types/ws": "8.18.1", - "@types/yargs": "17.0.34", - "babel-plugin-react-compiler": "^1.0.0", - "drizzle-kit": "0.31.6", - "esbuild": "0.27.0", - "esbuild-node-externals": "1.19.1", - "postcss": "^8", - "react-email": "4.3.2", - "tailwindcss": "^4.1.4", + "@types/yargs": "17.0.35", + "babel-plugin-react-compiler": "1.0.0", + "drizzle-kit": "0.31.8", + "esbuild": "0.27.1", + "esbuild-node-externals": "1.20.1", + "postcss": "8.5.6", + "prettier": "3.7.4", + "react-email": "5.0.6", + "tailwindcss": "4.1.17", "tsc-alias": "1.8.16", - "tsx": "4.20.6", - "typescript": "^5", - "typescript-eslint": "^8.46.3" + "tsx": "4.21.0", + "typescript": "5.9.3", + "typescript-eslint": "8.49.0" } }, "node_modules/@alloc/quick-lru": { @@ -182,9 +182,9 @@ } }, "node_modules/@asteasolutions/zod-to-openapi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.1.0.tgz", - "integrity": "sha512-tQFxVs05J/6QXXqIzj6rTRk3nj1HFs4pe+uThwE95jL5II2JfpVXkK+CqkO7aT0Do5AYqO6LDrKpleLUFXgY+g==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.2.0.tgz", + "integrity": "sha512-u05zNUirlukJAf9oEHmxSF31L1XQhz9XdpVILt7+xhrz65oQqBpiOWFkGvRWL0IpjOUJ878idKoNmYPxrFnkeg==", "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" @@ -396,115 +396,110 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.922.0.tgz", - "integrity": "sha512-SZRaZUUAHCWfEyBf4SRSPd29ko4uFoJpfd0E/w1meE68XhFB52FTtz/71UqYcwqZmN+s7oUNFFZT+DE/dnQSEA==", + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.947.0.tgz", + "integrity": "sha512-ICgnI8D3ccIX9alsLksPFY2bX5CAIbyB+q19sXJgPhzCJ5kWeQ6LQ5xBmRVT5kccmsVGbbJdhnLXHyiN5LZsWg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/credential-provider-node": "3.922.0", - "@aws-sdk/middleware-bucket-endpoint": "3.922.0", - "@aws-sdk/middleware-expect-continue": "3.922.0", - "@aws-sdk/middleware-flexible-checksums": "3.922.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-location-constraint": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-sdk-s3": "3.922.0", - "@aws-sdk/middleware-ssec": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/region-config-resolver": "3.922.0", - "@aws-sdk/signature-v4-multi-region": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.922.0", - "@aws-sdk/xml-builder": "3.921.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/core": "^3.17.2", - "@smithy/eventstream-serde-browser": "^4.2.4", - "@smithy/eventstream-serde-config-resolver": "^4.3.4", - "@smithy/eventstream-serde-node": "^4.2.4", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-blob-browser": "^4.2.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/hash-stream-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/md5-js": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/credential-provider-node": "3.947.0", + "@aws-sdk/middleware-bucket-endpoint": "3.936.0", + "@aws-sdk/middleware-expect-continue": "3.936.0", + "@aws-sdk/middleware-flexible-checksums": "3.947.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-location-constraint": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-sdk-s3": "3.947.0", + "@aws-sdk/middleware-ssec": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/signature-v4-multi-region": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/eventstream-serde-browser": "^4.2.5", + "@smithy/eventstream-serde-config-resolver": "^4.3.5", + "@smithy/eventstream-serde-node": "^4.2.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-blob-browser": "^4.2.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/hash-stream-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/md5-js": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.7", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", - "@smithy/util-stream": "^4.5.5", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.4", - "@smithy/uuid": "^1.1.0", + "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.943.0.tgz", - "integrity": "sha512-Zlyw/y5CbhhRK+ZdN/aMNPA8BYo427ddxkTG5yI/JasO05i7thPkhyqsUjyfsV6Z1tHfwFlLeicW57aLW8fAVw==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.947.0.tgz", + "integrity": "sha512-sDwcO8SP290WSErY1S8pz8hTafeghKmmWjNVks86jDK30wx62CfazOTeU70IpWgrUBEygyXk/zPogHsUMbW2Rg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.943.0", - "@aws-sdk/credential-provider-node": "3.943.0", + "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/signature-v4-multi-region": "3.943.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.943.0", + "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.11", - "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", @@ -515,71 +510,20 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.943.0.tgz", - "integrity": "sha512-kOTO2B8Ks2qX73CyKY8PAajtf5n39aMe2spoiOF5EkgSzGV7hZ/HONRDyADlyxwfsX39Q2F2SpPUaXzon32IGw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.943.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.943.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.943.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-retry": "^4.4.12", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.11", - "@smithy/util-defaults-mode-node": "^4.2.14", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.943.0.tgz", - "integrity": "sha512-8CBy2hI9ABF7RBVQuY1bgf/ue+WPmM/hl0adrXFlhnhkaQP0tFY5zhiy1Y+n7V+5f3/ORoHBmCCQmcHDDYJqJQ==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", + "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", @@ -590,14 +534,13 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.943.0.tgz", - "integrity": "sha512-WnS5w9fK9CTuoZRVSIHLOMcI63oODg9qd1vXMYb7QGLGlfwUm4aG3hdu7i9XvYrpkQfE3dzwWLtXF4ZBuL1Tew==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", + "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", @@ -607,20 +550,19 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.943.0.tgz", - "integrity": "sha512-SA8bUcYDEACdhnhLpZNnWusBpdmj4Vl67Vxp3Zke7SvoWSYbuxa+tiDiC+c92Z4Yq6xNOuLPW912ZPb9/NsSkA==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", + "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" @@ -629,21 +571,20 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.943.0.tgz", - "integrity": "sha512-BcLDb8l4oVW+NkuqXMlO7TnM6lBOWW318ylf4FRED/ply5eaGxkQYqdGvHSqGSN5Rb3vr5Ek0xpzSjeYD7C8Kw==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.947.0.tgz", + "integrity": "sha512-A2ZUgJUJZERjSzvCi2NR/hBVbVkTXPD0SdKcR/aITb30XwF+n3T963b+pJl90qhOspoy7h0IVYNR7u5Nr9tJdQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", - "@aws-sdk/credential-provider-env": "3.943.0", - "@aws-sdk/credential-provider-http": "3.943.0", - "@aws-sdk/credential-provider-login": "3.943.0", - "@aws-sdk/credential-provider-process": "3.943.0", - "@aws-sdk/credential-provider-sso": "3.943.0", - "@aws-sdk/credential-provider-web-identity": "3.943.0", - "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/credential-provider-env": "3.947.0", + "@aws-sdk/credential-provider-http": "3.947.0", + "@aws-sdk/credential-provider-login": "3.947.0", + "@aws-sdk/credential-provider-process": "3.947.0", + "@aws-sdk/credential-provider-sso": "3.947.0", + "@aws-sdk/credential-provider-web-identity": "3.947.0", + "@aws-sdk/nested-clients": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", @@ -655,19 +596,37 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.943.0.tgz", - "integrity": "sha512-14eddaH/gjCWoLSAELVrFOQNyswUYwWphIt+PdsJ/FqVfP4ay2HsiZVEIYbQtmrKHaoLJhiZKwBQRjcqJDZG0w==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.947.0.tgz", + "integrity": "sha512-u7M3hazcB7aJiVwosNdJRbIJDzbwQ861NTtl6S0HmvWpixaVb7iyhJZWg8/plyUznboZGBm7JVEdxtxv3u0bTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.943.0", - "@aws-sdk/credential-provider-http": "3.943.0", - "@aws-sdk/credential-provider-ini": "3.943.0", - "@aws-sdk/credential-provider-process": "3.943.0", - "@aws-sdk/credential-provider-sso": "3.943.0", - "@aws-sdk/credential-provider-web-identity": "3.943.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.947.0.tgz", + "integrity": "sha512-S0Zqebr71KyrT6J4uYPhwV65g4V5uDPHnd7dt2W34FcyPu+hVC7Hx4MFmsPyVLeT5cMCkkZvmY3kAoEzgUPJJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.947.0", + "@aws-sdk/credential-provider-http": "3.947.0", + "@aws-sdk/credential-provider-ini": "3.947.0", + "@aws-sdk/credential-provider-process": "3.947.0", + "@aws-sdk/credential-provider-sso": "3.947.0", + "@aws-sdk/credential-provider-web-identity": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", @@ -679,14 +638,13 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.943.0.tgz", - "integrity": "sha512-GIY/vUkthL33AdjOJ8r9vOosKf/3X+X7LIiACzGxvZZrtoOiRq0LADppdiKIB48vTL63VvW+eRIOFAxE6UDekw==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", + "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -697,16 +655,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.943.0.tgz", - "integrity": "sha512-1c5G11syUrru3D9OO6Uk+ul5e2lX1adb+7zQNyluNaLPXP6Dina6Sy6DFGRLu7tM8+M7luYmbS3w63rpYpaL+A==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.947.0.tgz", + "integrity": "sha512-NktnVHTGaUMaozxycYrepvb3yfFquHTQ53lt6hBEVjYBzK3C4tVz0siUpr+5RMGLSiZ5bLBp2UjJPgwx4i4waQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.943.0", - "@aws-sdk/core": "3.943.0", - "@aws-sdk/token-providers": "3.943.0", + "@aws-sdk/client-sso": "3.947.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/token-providers": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -717,15 +674,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.943.0.tgz", - "integrity": "sha512-VtyGKHxICSb4kKGuaqotxso8JVM8RjCS3UYdIMOxUt9TaFE/CZIfZKtjTr+IJ7M0P7t36wuSUb/jRLyNmGzUUA==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.947.0.tgz", + "integrity": "sha512-gokm/e/YHiHLrZgLq4j8tNAn8RJDPbIcglFRKgy08q8DmAqHQ8MXAKW3eS0QjAuRXU9mcMmUo1NrX6FRNBCCPw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", - "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -736,69 +692,20 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", - "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.947.0.tgz", + "integrity": "sha512-DS2tm5YBKhPW2PthrRBDr6eufChbwXe0NjtTZcYDfUCXf0OR+W6cIqyKguwHMJ+IyYdey30AfVw9/Lb5KB8U8A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-logger": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", - "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", - "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws/lambda-invoke-store": "^0.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.943.0.tgz", - "integrity": "sha512-kd2mALfthU+RS9NsPS+qvznFcPnVgVx9mgmStWCPn5Qc5BTnx4UAtm+HPA+XZs+zxOopp+zmAfE4qxDHRVONBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.5", @@ -810,17 +717,16 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.943.0.tgz", - "integrity": "sha512-956n4kVEwFNXndXfhSAN5wO+KRgqiWEEY+ECwLvxmmO8uQ0NWOa8l6l65nTtyuiWzMX81c9BvlyNR5EgUeeUvA==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", + "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" @@ -829,46 +735,45 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.943.0.tgz", - "integrity": "sha512-anFtB0p2FPuyUnbOULwGmKYqYKSq1M73c9uZ08jR/NCq6Trjq9cuF5TFTeHwjJyPRb4wMf2Qk859oiVfFqnQiw==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.947.0.tgz", + "integrity": "sha512-DjRJEYNnHUTu9kGPPQDTSXquwSEd6myKR4ssI4FaYLFhdT3ldWpj73yYt807H3tdmhS7vPmdVqchSJnjurUQAw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.943.0", + "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.11", - "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", @@ -879,31 +784,13 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", - "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.947.0.tgz", + "integrity": "sha512-UaYmzoxf9q3mabIA2hc4T6x5YSFUG2BpNjAZ207EA1bnQMiK+d6vZvb83t7dIWL/U1de1sGV19c1C81Jf14rrA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.943.0.tgz", - "integrity": "sha512-KKvmxNQ/FZbM6ml6nKd8ltDulsUojsXnMJNgf1VHTcJEbADC/6mVWOq0+e9D0WP1qixUBEuMjlS2HqD5KoqwEg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.943.0", + "@aws-sdk/middleware-sdk-s3": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", @@ -914,15 +801,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.943.0.tgz", - "integrity": "sha512-cRKyIzwfkS+XztXIFPoWORuaxlIswP+a83BJzelX4S1gUZ7FcXB4+lj9Jxjn8SbQhR4TPU3Owbpu+S7pd6IRbQ==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.947.0.tgz", + "integrity": "sha512-X/DyB8GuK44rsE89Tn5+s542B3PhGbXQSgV8lvqHDzvicwCt0tWny6790st6CPETrVVV2K3oJMfG5U3/jAmaZA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", - "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", @@ -933,58 +819,13 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/types": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", - "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", - "dev": true, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", + "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", - "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-endpoints": "^3.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", - "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.943.0.tgz", - "integrity": "sha512-gn+ILprVRrgAgTIBk2TDsJLRClzIOdStQFeFTcN0qpL8Z4GBCqMFhw7O7X+MM55Stt5s4jAauQ/VvoqmCADnQg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", @@ -1002,73 +843,101 @@ } } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/xml-builder": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", - "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.946.0.tgz", + "integrity": "sha512-JYj3BPqgyRXgBjZ3Xvo4Abd+vLxcsHe4gb0TvwiSM/k7e6MRgBZoYwDOnwbNDs/62X1sn7MPHqqB3miuO4nR5g==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/credential-provider-node": "3.946.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.946.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/signature-v4-multi-region": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.946.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", - "fast-xml-parser": "5.2.5", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sesv2/node_modules/@aws/lambda-invoke-store": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", - "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/client-sso": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.922.0.tgz", - "integrity": "sha512-jdHs7uy7cSpiMvrxhYmqHyJxgK7hyqw4plG8OQ4YTBpq0SbfAxdoOuOkwJ1IVUUQho4otR1xYYjiX/8e8J8qwQ==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.946.0.tgz", + "integrity": "sha512-kGAs5iIVyUz4p6TX3pzG5q3cNxXnVpC4pwRC6DCSaSv9ozyPjc2d74FsK4fZ+J+ejtvCdJk72uiuQtWJc86Wuw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/region-config-resolver": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.922.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.946.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.946.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.7", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -1077,22 +946,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", - "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.946.0.tgz", + "integrity": "sha512-u2BkbLLVbMFrEiXrko2+S6ih5sUZPlbVyRPtXOqMHlCyzr70sE8kIiD6ba223rQeIFPcYfW/wHc6k4ihW2xxVg==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws-sdk/xml-builder": "3.921.0", - "@smithy/core": "^3.17.2", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/signature-v4": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.7", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.4", + "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -1101,15 +971,16 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", - "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.946.0.tgz", + "integrity": "sha512-P4l+K6wX1tf8LmWUvZofdQ+BgCNyk6Tb9u1H10npvqpuCD+dCM4pXIBq3PQcv/juUBOvLGGREo+Govuh3lfD0Q==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/types": "^4.8.1", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1117,20 +988,21 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", - "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.946.0.tgz", + "integrity": "sha512-/zeOJ6E7dGZQ/l2k7KytEoPJX0APIhwt0A79hPf/bUpMF4dDs2P6JmchDrotk0a0Y/MIdNF8sBQ/MEOPnBiYoQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/util-stream": "^4.5.5", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" }, "engines": { @@ -1138,23 +1010,25 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.922.0.tgz", - "integrity": "sha512-bVF+pI5UCLNkvbiZr/t2fgTtv84s8FCdOGAPxQiQcw5qOZywNuuCCY3wIIchmQr6GJr8YFkEp5LgDCac5EC5aQ==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.946.0.tgz", + "integrity": "sha512-Pdgcra3RivWj/TuZmfFaHbqsvvgnSKO0CxlRUMMr0PgBiCnUhyl+zBktdNOeGsOPH2fUzQpYhcUjYUgVSdcSDQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/credential-provider-env": "3.922.0", - "@aws-sdk/credential-provider-http": "3.922.0", - "@aws-sdk/credential-provider-process": "3.922.0", - "@aws-sdk/credential-provider-sso": "3.922.0", - "@aws-sdk/credential-provider-web-identity": "3.922.0", - "@aws-sdk/nested-clients": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/credential-provider-imds": "^4.2.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/credential-provider-env": "3.946.0", + "@aws-sdk/credential-provider-http": "3.946.0", + "@aws-sdk/credential-provider-login": "3.946.0", + "@aws-sdk/credential-provider-process": "3.946.0", + "@aws-sdk/credential-provider-sso": "3.946.0", + "@aws-sdk/credential-provider-web-identity": "3.946.0", + "@aws-sdk/nested-clients": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1162,14 +1036,14 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.943.0.tgz", - "integrity": "sha512-9iCOVkiRW+evxiJE94RqosCwRrzptAVPhRhGWv4osfYDhjNAvUMyrnZl3T1bjqCoKNcETRKEZIU3dqYHnUkcwQ==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.946.0.tgz", + "integrity": "sha512-5iqLNc15u2Zx+7jOdQkIbP62N7n2031tw5hkmIG0DLnozhnk64osOh2CliiOE9x3c4P9Pf4frAwgyy9GzNTk2g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", - "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/nested-clients": "3.946.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", @@ -1181,21 +1055,158 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.943.0.tgz", - "integrity": "sha512-8CBy2hI9ABF7RBVQuY1bgf/ue+WPmM/hl0adrXFlhnhkaQP0tFY5zhiy1Y+n7V+5f3/ORoHBmCCQmcHDDYJqJQ==", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.946.0.tgz", + "integrity": "sha512-I7URUqnBPng1a5y81OImxrwERysZqMBREG6svhhGeZgxmqcpAZ8z5ywILeQXdEOCuuES8phUp/ojzxFjPXp/eA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.946.0", + "@aws-sdk/credential-provider-http": "3.946.0", + "@aws-sdk/credential-provider-ini": "3.946.0", + "@aws-sdk/credential-provider-process": "3.946.0", + "@aws-sdk/credential-provider-sso": "3.946.0", + "@aws-sdk/credential-provider-web-identity": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.946.0.tgz", + "integrity": "sha512-GtGHX7OGqIeVQ3DlVm5RRF43Qmf3S1+PLJv9svrdvAhAdy2bUb044FdXXqrtSsIfpzTKlHgQUiRo5MWLd35Ntw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.946.0.tgz", + "integrity": "sha512-LeGSSt2V5iwYey1ENGY75RmoDP3bA2iE/py8QBKW8EDA8hn74XBLkprhrK5iccOvU3UGWY8WrEKFAFGNjJOL9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.946.0", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/token-providers": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.946.0.tgz", + "integrity": "sha512-ocBCvjWfkbjxElBI1QUxOnHldsNhoU0uOICFvuRDAZAoxvypJHN3m5BJkqb7gqorBbcv3LRgmBdEnWXOAvq+7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", + "@aws-sdk/nested-clients": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.936.0.tgz", + "integrity": "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.936.0.tgz", + "integrity": "sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.947.0.tgz", + "integrity": "sha512-kXXxS2raNESNO+zR0L4YInVjhcGGNI2Mx0AE1ThRhDkAt2se3a+rGf9equ9YvOqA1m8Jl/GSI8cXYvSxXmS9Ag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", + "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", @@ -1206,11 +1217,10 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/middleware-host-header": { "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1222,11 +1232,10 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/middleware-location-constraint": { "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", - "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", - "dev": true, + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.936.0.tgz", + "integrity": "sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1237,11 +1246,24 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1254,17 +1276,57 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.943.0.tgz", - "integrity": "sha512-956n4kVEwFNXndXfhSAN5wO+KRgqiWEEY+ECwLvxmmO8uQ0NWOa8l6l65nTtyuiWzMX81c9BvlyNR5EgUeeUvA==", + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.946.0.tgz", + "integrity": "sha512-0UTFmFd8PX2k/jLu/DBmR+mmLQWAtUGHYps9Rjx3dcXNwaMLaa/39NoV3qn7Dwzfpqc6JZlZzBk+NDOCJIHW9g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.18.7", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.936.0.tgz", + "integrity": "sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.946.0.tgz", + "integrity": "sha512-7QcljCraeaWQNuqmOoAyZs8KpZcuhPiqdeeKoRd397jVGNRehLFsZbIMOvwaluUDFY11oMyXOkQEERe1Zo2fCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.946.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" @@ -1273,46 +1335,46 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.943.0.tgz", - "integrity": "sha512-anFtB0p2FPuyUnbOULwGmKYqYKSq1M73c9uZ08jR/NCq6Trjq9cuF5TFTeHwjJyPRb4wMf2Qk859oiVfFqnQiw==", + "node_modules/@aws-sdk/nested-clients": { + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.946.0.tgz", + "integrity": "sha512-rjAtEguukeW8mlyEQMQI56vxFoyWlaNwowmz1p1rav948SUjtrzjHAp4TOQWhibb7AR7BUTHBCgIcyCRjBEf4g==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.943.0", + "@aws-sdk/core": "3.946.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/middleware-user-agent": "3.946.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.943.0", + "@aws-sdk/util-user-agent-node": "3.946.0", "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.5", + "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", + "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.11", - "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", @@ -1323,11 +1385,10 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/region-config-resolver": { "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1340,426 +1401,18 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", - "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", - "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-endpoints": "^3.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", - "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.943.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.943.0.tgz", - "integrity": "sha512-gn+ILprVRrgAgTIBk2TDsJLRClzIOdStQFeFTcN0qpL8Z4GBCqMFhw7O7X+MM55Stt5s4jAauQ/VvoqmCADnQg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.943.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", - "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", - "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.922.0.tgz", - "integrity": "sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.922.0", - "@aws-sdk/credential-provider-http": "3.922.0", - "@aws-sdk/credential-provider-ini": "3.922.0", - "@aws-sdk/credential-provider-process": "3.922.0", - "@aws-sdk/credential-provider-sso": "3.922.0", - "@aws-sdk/credential-provider-web-identity": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/credential-provider-imds": "^4.2.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", - "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.922.0.tgz", - "integrity": "sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.922.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/token-providers": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.922.0.tgz", - "integrity": "sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/nested-clients": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.922.0.tgz", - "integrity": "sha512-Dpr2YeOaLFqt3q1hocwBesynE3x8/dXZqXZRuzSX/9/VQcwYBFChHAm4mTAl4zuvArtDbLrwzWSxmOWYZGtq5w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "@smithy/util-config-provider": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.922.0.tgz", - "integrity": "sha512-xmnLWMtmHJHJBupSWMUEW1gyxuRIeQ1Ov2xa8Tqq77fPr4Ft2AluEwiDMaZIMHoAvpxWKEEt9Si59Li7GIA+bQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.922.0.tgz", - "integrity": "sha512-G363np7YcJhf+gBucskdv8cOTbs2TRwocEzRupuqDIooGDlLBlfJrvwehdgtWR8l53yjJR3zcHvGrVPTe2h8Nw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@aws-crypto/crc32c": "5.2.0", - "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-stream": "^4.5.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", - "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.922.0.tgz", - "integrity": "sha512-T4iqd7WQ2DDjCH/0s50mnhdoX+IJns83ZE+3zj9IDlpU0N2aq8R91IG890qTfYkUEdP9yRm0xir/CNed+v6Dew==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", - "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", - "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws/lambda-invoke-store": "^0.1.1", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.922.0.tgz", - "integrity": "sha512-ygg8lME1oFAbsH42ed2wtGqfHLoT5irgx6VC4X98j79fV1qXEwwwbqMsAiMQ/HJehpjqAFRVsHox3MHLN48Z5A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.17.2", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/signature-v4": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-stream": "^4.5.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.922.0.tgz", - "integrity": "sha512-eHvSJZTSRJO+/tjjGD6ocnPc8q9o3m26+qbwQTu/4V6yOJQ1q+xkDZNqwJQphL+CodYaQ7uljp8g1Ji/AN3D9w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", - "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@smithy/core": "^3.17.2", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.922.0.tgz", - "integrity": "sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/region-config-resolver": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.922.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.7", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.922.0.tgz", - "integrity": "sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.922.0.tgz", - "integrity": "sha512-mmsgEEL5pE+A7gFYiJMDBCLVciaXq4EFI5iAP7bPpnHvOplnNOYxVy2IreKMllGvrfjVyLnwxzZYlo5zZ65FWg==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.946.0.tgz", + "integrity": "sha512-61FZ685lKiJuQ06g6U7K3PL9EwKCxNm51wNlxyKV57nnl1GrLD0NC8O3/hDNkCQLNBArT9y3IXl2H7TtIxP8Jg==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/signature-v4": "^5.3.4", - "@smithy/types": "^4.8.1", + "@aws-sdk/middleware-sdk-s3": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1767,17 +1420,18 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.922.0.tgz", - "integrity": "sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.946.0.tgz", + "integrity": "sha512-a5c+rM6CUPX2ExmUZ3DlbLlS5rQr4tbdoGcgBsjnAHiYx8MuMNAI+8M7wfjF13i2yvUQj5WEIddvLpayfEZj9g==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/nested-clients": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", + "@aws-sdk/core": "3.946.0", + "@aws-sdk/nested-clients": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1785,12 +1439,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", - "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1810,15 +1464,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", - "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-endpoints": "^3.2.4", + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1838,27 +1492,28 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", - "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", - "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", + "version": "3.946.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.946.0.tgz", + "integrity": "sha512-a2UwwvzbK5AxHKUBupfg4s7VnkqRAHjYsuezHnKCniczmT4HZfP1NnfwwvLKEH8qaTrwenxjKSfq4UWmWkvG+Q==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", + "@aws-sdk/middleware-user-agent": "3.946.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1874,12 +1529,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.921.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", - "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, @@ -1888,9 +1543,9 @@ } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -1924,6 +1579,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -2315,7 +1971,6 @@ "@noble/ciphers": "^1.0.0" } }, -<<<<<<< HEAD "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", @@ -2347,8 +2002,6 @@ "tslib": "^2.4.0" } }, -======= ->>>>>>> dev "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", @@ -2802,9 +2455,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", "cpu": [ "ppc64" ], @@ -2819,9 +2472,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", "cpu": [ "arm" ], @@ -2836,9 +2489,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", "cpu": [ "arm64" ], @@ -2853,9 +2506,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", "cpu": [ "x64" ], @@ -2870,9 +2523,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", "cpu": [ "arm64" ], @@ -2887,9 +2540,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", "cpu": [ "x64" ], @@ -2904,9 +2557,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", "cpu": [ "arm64" ], @@ -2921,9 +2574,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", "cpu": [ "x64" ], @@ -2938,9 +2591,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", "cpu": [ "arm" ], @@ -2955,9 +2608,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", "cpu": [ "arm64" ], @@ -2972,9 +2625,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", "cpu": [ "ia32" ], @@ -2989,9 +2642,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", "cpu": [ "loong64" ], @@ -3006,9 +2659,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", "cpu": [ "mips64el" ], @@ -3023,9 +2676,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", "cpu": [ "ppc64" ], @@ -3040,9 +2693,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", "cpu": [ "riscv64" ], @@ -3057,9 +2710,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", "cpu": [ "s390x" ], @@ -3074,9 +2727,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", "cpu": [ "x64" ], @@ -3091,9 +2744,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", "cpu": [ "arm64" ], @@ -3108,9 +2761,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", "cpu": [ "x64" ], @@ -3125,9 +2778,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", "cpu": [ "arm64" ], @@ -3142,9 +2795,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", "cpu": [ "x64" ], @@ -3159,9 +2812,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", "cpu": [ "arm64" ], @@ -3176,9 +2829,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", "cpu": [ "x64" ], @@ -3193,9 +2846,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", "cpu": [ "arm64" ], @@ -3210,9 +2863,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", "cpu": [ "ia32" ], @@ -3227,9 +2880,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", "cpu": [ "x64" ], @@ -3608,20 +3261,95 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "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.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", "cpu": [ "x64" ], - "dev": true, + "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.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "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.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "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.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3631,6 +3359,190 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "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.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "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.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "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.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "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.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "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.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "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.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "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.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "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.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "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.3" + } + }, "node_modules/@img/sharp-linux-x64": { "version": "0.34.4", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", @@ -3638,7 +3550,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3654,6 +3565,126 @@ "@img/sharp-libvips-linux-x64": "1.2.3" } }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "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.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "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.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "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.4.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", @@ -3685,6 +3716,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3728,17 +3760,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -3761,26 +3782,6 @@ "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", "license": "MIT" }, - "node_modules/@lottiefiles/dotlottie-react": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.3.tgz", - "integrity": "sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@lottiefiles/dotlottie-web": "0.42.0" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19" - } - }, - "node_modules/@lottiefiles/dotlottie-web": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.42.0.tgz", - "integrity": "sha512-Zr2LCaOAoPCsdAQgeLyCSiQ1+xrAJtRCyuEYDj0qR5heUwpc+Pxbb88JyTVumcXFfKOBMOMmrlsTScLz2mrvQQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@monaco-editor/loader": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", @@ -3804,6 +3805,18 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "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.5.7", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", @@ -3811,15 +3824,14 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.3.tgz", - "integrity": "sha512-6sPWmZetzFWMsz7Dhuxsdmbu3fK+/AxKRtj7OB0/3OZAI2MHB/v2FeYh271LZ9abvnM1WIwWc/5umYjx0jo5sQ==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.8.tgz", + "integrity": "sha512-1miV0qXDcLUaOdHridVPCh4i39ElRIAraseVIbb3BEqyZ5ol9sPyjTP/GNTPV5rBxqxjF6/vv5zQTVbhiNaLqA==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } }, -<<<<<<< HEAD "node_modules/@next/swc-darwin-arm64": { "version": "15.5.7", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", @@ -3884,8 +3896,6 @@ "node": ">= 10" } }, -======= ->>>>>>> dev "node_modules/@next/swc-linux-x64-gnu": { "version": "15.5.7", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", @@ -3902,7 +3912,6 @@ "node": ">= 10" } }, -<<<<<<< HEAD "node_modules/@next/swc-linux-x64-musl": { "version": "15.5.7", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", @@ -3951,14 +3960,13 @@ "node": ">= 10" } }, -======= ->>>>>>> dev "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -4020,6 +4028,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", @@ -4036,6 +4172,86 @@ "node": ">= 10" } }, + "node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", + "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "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", @@ -4065,6 +4281,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", @@ -4081,6 +4425,119 @@ "node": ">= 10" } }, + "node_modules/@node-rs/bcrypt-linux-x64-musl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.9.0.tgz", + "integrity": "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "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", @@ -4309,22 +4766,25 @@ } }, "node_modules/@peculiar/x509": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.0.tgz", - "integrity": "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.2.tgz", + "integrity": "sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.5.0", - "@peculiar/asn1-csr": "^2.5.0", - "@peculiar/asn1-ecc": "^2.5.0", - "@peculiar/asn1-pkcs9": "^2.5.0", - "@peculiar/asn1-rsa": "^2.5.0", - "@peculiar/asn1-schema": "^2.5.0", - "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=22.0.0" } }, "node_modules/@posthog/core": { @@ -4336,13 +4796,6 @@ "cross-spawn": "^7.0.6" } }, - "node_modules/@radix-ui/colors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", - "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", - "dev": true, - "license": "MIT" - }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -6197,164 +6650,6 @@ } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -6737,9 +7032,9 @@ } }, "node_modules/@react-email/body": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.1.0.tgz", - "integrity": "sha512-o1bcSAmDYNNHECbkeyceCVPGmVsYvT+O3sSO/Ct7apKUu3JphTi31hu+0Nwqr/pgV5QFqdoT5vdS3SW5DJFHgQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.0.tgz", + "integrity": "sha512-9GCWmVmKUAoRfloboCd+RKm6X17xn7eGL7HnpAZUnjBXBilWCxsKnLMTC/ixSHDKS/A/057M1Tx6ZUXd89sVBw==", "license": "MIT", "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -6758,15 +7053,15 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.1.0.tgz", - "integrity": "sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.0.tgz", + "integrity": "sha512-eIrPW9PIFgDopQU0e/OPpwCW2QWQDtNZDSsiN4sJO8KdMnWWnXJicnRfzrit5rHwFo+Y98i+w/Y5ScnBAFr1dQ==", "license": "MIT", "dependencies": { "prismjs": "^1.30.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -6797,14 +7092,14 @@ } }, "node_modules/@react-email/components": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.5.7.tgz", - "integrity": "sha512-ECyVoyDcev2FSQ7C0buXaIJ0+6MRDXNUbCOZwBRrlLdCCRjap2b4+MHrYSTXFzo5kqfjjRoyo/2PbJXFQni67g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.1.tgz", + "integrity": "sha512-HnL0Y/up61sOBQT2cQg9N/kCoW0bP727gDs2MkFWQYELg6+iIHidMDvENXFC0f1ZE6hTB+4t7sszptvTcJWsDA==", "license": "MIT", "dependencies": { - "@react-email/body": "0.1.0", + "@react-email/body": "0.2.0", "@react-email/button": "0.2.0", - "@react-email/code-block": "0.1.0", + "@react-email/code-block": "0.2.0", "@react-email/code-inline": "0.0.5", "@react-email/column": "0.0.13", "@react-email/container": "0.0.15", @@ -6815,16 +7110,16 @@ "@react-email/html": "0.0.11", "@react-email/img": "0.0.11", "@react-email/link": "0.0.12", - "@react-email/markdown": "0.0.16", + "@react-email/markdown": "0.0.17", "@react-email/preview": "0.0.13", - "@react-email/render": "1.4.0", + "@react-email/render": "2.0.0", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "1.2.2", + "@react-email/tailwind": "2.0.1", "@react-email/text": "0.1.5" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -6924,15 +7219,15 @@ } }, "node_modules/@react-email/markdown": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.16.tgz", - "integrity": "sha512-KSUHmoBMYhvc6iGwlIDkm0DRGbGQ824iNjLMCJsBVUoKHGQYs7F/N3b1tnS1YzRUX+GwHIexSsHuIUEi1m+8OQ==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.17.tgz", + "integrity": "sha512-6op3AfsBC9BJKkhG+eoMFRFWlr0/f3FYbtQrK+VhGzJocEAY0WINIFN+W8xzXr//3IL0K/aKtnH3FtpIuescQQ==", "license": "MIT", "dependencies": { "marked": "^15.0.12" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -6950,937 +7245,17 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, - "node_modules/@react-email/preview-server": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-4.3.2.tgz", - "integrity": "sha512-rBm2AJhOhfi8Fd8MAFN4DQ0FQtsqq38JjJIWvbHA0EYwbjNwODmtzRZCkbdp+8o6GL5PKiRcikF0FDzbOYAJ+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "7.26.10", - "@babel/parser": "7.27.0", - "@babel/traverse": "7.27.0", - "@lottiefiles/dotlottie-react": "0.13.3", - "@radix-ui/colors": "3.0.0", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@types/node": "22.14.1", - "@types/normalize-path": "3.0.2", - "@types/react": "19.0.10", - "@types/react-dom": "19.0.4", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.21", - "clsx": "2.1.1", - "esbuild": "0.25.10", - "framer-motion": "12.23.22", - "json5": "2.2.3", - "log-symbols": "4.1.0", - "module-punycode": "npm:punycode@2.3.1", - "next": "15.5.2", - "node-html-parser": "7.0.1", - "ora": "5.4.1", - "pretty-bytes": "6.1.1", - "prism-react-renderer": "2.4.1", - "react": "19.0.0", - "react-dom": "19.0.0", - "sharp": "0.34.4", - "socket.io-client": "4.8.1", - "sonner": "2.0.3", - "source-map-js": "1.2.1", - "spamc": "0.0.5", - "stacktrace-parser": "0.1.11", - "tailwind-merge": "3.2.0", - "tailwindcss": "3.4.0", - "use-debounce": "10.0.4", - "zod": "3.24.3" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/env": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", - "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", - "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@react-email/preview-server/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@types/react": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", - "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@react-email/preview-server/node_modules/@types/react-dom": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", - "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/@react-email/preview-server/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@react-email/preview-server/node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" - } - }, - "node_modules/@react-email/preview-server/node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/@react-email/preview-server/node_modules/next": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", - "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/env": "15.5.2", - "@swc/helpers": "0.5.15", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.2", - "@next/swc-darwin-x64": "15.5.2", - "@next/swc-linux-arm64-gnu": "15.5.2", - "@next/swc-linux-arm64-musl": "15.5.2", - "@next/swc-linux-x64-gnu": "15.5.2", - "@next/swc-linux-x64-musl": "15.5.2", - "@next/swc-win32-arm64-msvc": "15.5.2", - "@next/swc-win32-x64-msvc": "15.5.2", - "sharp": "^0.34.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/@react-email/preview-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@react-email/preview-server/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/@react-email/preview-server/node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "scheduler": "^0.25.0" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/preview-server/node_modules/tailwind-merge": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", - "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/@react-email/preview-server/node_modules/tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/tailwindcss/node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/@react-email/preview-server/node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/@react-email/preview-server/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/preview-server/node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@react-email/render": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.4.0.tgz", - "integrity": "sha512-ZtJ3noggIvW1ZAryoui95KJENKdCzLmN5F7hyZY1F/17B1vwzuxHB7YkuCg0QqHjDivc5axqYEYdIOw4JIQdUw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.0.tgz", + "integrity": "sha512-rdjNj6iVzv8kRKDPFas+47nnoe6B40+nwukuXwY4FCwM7XBg6tmYr+chQryCuavUj2J65MMf6fztk1bxOUiSVA==", "license": "MIT", "dependencies": { "html-to-text": "^9.0.5", - "prettier": "^3.5.3", - "react-promise-suspense": "^0.3.4" + "prettier": "^3.5.3" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", @@ -7912,15 +7287,61 @@ } }, "node_modules/@react-email/tailwind": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.2.2.tgz", - "integrity": "sha512-heO9Khaqxm6Ulm6p7HQ9h01oiiLRrZuuEQuYds/O7Iyp3c58sMVHZGIxiRXO/kSs857NZQycpjewEVKF3jhNTw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.1.tgz", + "integrity": "sha512-/xq0IDYVY7863xPY7cdI45Xoz7M6CnIQBJcQvbqN7MNVpopfH9f+mhjayV1JGfKaxlGWuxfLKhgi9T2shsnEFg==", "license": "MIT", + "dependencies": { + "tailwindcss": "^4.1.12" + }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { + "@react-email/body": "0.2.0", + "@react-email/button": "0.2.0", + "@react-email/code-block": "0.2.0", + "@react-email/code-inline": "0.0.5", + "@react-email/container": "0.0.15", + "@react-email/heading": "0.0.15", + "@react-email/hr": "0.0.11", + "@react-email/img": "0.0.11", + "@react-email/link": "0.0.12", + "@react-email/preview": "0.0.13", + "@react-email/text": "0.1.5", "react": "^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@react-email/body": { + "optional": true + }, + "@react-email/button": { + "optional": true + }, + "@react-email/code-block": { + "optional": true + }, + "@react-email/code-inline": { + "optional": true + }, + "@react-email/container": { + "optional": true + }, + "@react-email/heading": { + "optional": true + }, + "@react-email/hr": { + "optional": true + }, + "@react-email/img": { + "optional": true + }, + "@react-email/link": { + "optional": true + }, + "@react-email/preview": { + "optional": true + } } }, "node_modules/@react-email/text": { @@ -7928,6 +7349,7 @@ "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -8783,7 +8205,6 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, -<<<<<<< HEAD "node_modules/@swc/core": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", @@ -8902,8 +8323,6 @@ "node": ">=10" } }, -======= ->>>>>>> dev "node_modules/@swc/core-linux-x64-gnu": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz", @@ -8920,7 +8339,6 @@ "node": ">=10" } }, -<<<<<<< HEAD "node_modules/@swc/core-linux-x64-musl": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz", @@ -8985,8 +8403,6 @@ "node": ">=10" } }, -======= ->>>>>>> dev "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -9063,6 +8479,125 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", @@ -9080,7 +8615,6 @@ "node": ">= 10" } }, -<<<<<<< HEAD "node_modules/@tailwindcss/oxide-linux-x64-musl": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", @@ -9128,6 +8662,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.6.0", + "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.1.0", + "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": "1.0.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "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.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", @@ -9162,8 +8756,6 @@ "node": ">= 10" } }, -======= ->>>>>>> dev "node_modules/@tailwindcss/postcss": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", @@ -9204,6 +8796,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz", "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.12" }, @@ -9293,12 +8886,23 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "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", - "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -9626,28 +9230,6 @@ "@types/d3-selection": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -9655,15 +9237,16 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", - "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^1" + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { @@ -9740,13 +9323,6 @@ "@types/node": "*" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -9755,19 +9331,20 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "24.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz", + "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/@types/nodemailer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.3.tgz", - "integrity": "sha512-fC8w49YQ868IuPWRXqPfLf+MuTRex5Z1qxMoG8rr70riqqbOp2F5xgOKE9fODEBPzpnvjkJXFgK6IL2xgMSTnA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==", "dev": true, "license": "MIT", "dependencies": { @@ -9775,13 +9352,6 @@ "@types/node": "*" } }, - "node_modules/@types/normalize-path": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.2.tgz", - "integrity": "sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/nprogress": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz", @@ -9795,19 +9365,13 @@ "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -9823,21 +9387,23 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -9860,25 +9426,13 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -9925,20 +9479,7 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } + "optional": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -9951,9 +9492,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.34", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", - "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, "license": "MIT", "dependencies": { @@ -9968,17 +9509,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", + "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/type-utils": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -9991,7 +9531,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.1", + "@typescript-eslint/parser": "^8.49.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -10006,15 +9546,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", + "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4" }, "engines": { @@ -10030,13 +9571,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", + "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/tsconfig-utils": "^8.49.0", + "@typescript-eslint/types": "^8.49.0", "debug": "^4.3.4" }, "engines": { @@ -10051,13 +9592,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", + "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10068,9 +9609,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", + "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10084,14 +9625,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", + "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -10108,9 +9649,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10121,15 +9662,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", + "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/project-service": "8.49.0", + "@typescript-eslint/tsconfig-utils": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -10172,15 +9713,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", + "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10195,12 +9736,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", + "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/types": "8.49.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -10211,6 +9752,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", @@ -10224,180 +9934,73 @@ "linux" ] }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, + "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": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, + "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", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, + "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", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, + "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", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" + "optional": true, + "os": [ + "win32" + ] }, "node_modules/accepts": { "version": "2.0.0", @@ -10417,6 +10020,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10424,19 +10028,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -10463,9 +10054,9 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10531,13 +10122,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -10576,13 +10160,6 @@ "@oslojs/jwt": "0.2.0" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -10813,42 +10390,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "node_modules/atomically": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", + "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" } }, "node_modules/available-typed-arrays": { @@ -10901,6 +10451,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -10942,20 +10493,21 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.2.tgz", - "integrity": "sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw==", + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz", + "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "node_modules/better-sqlite3": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.0.tgz", - "integrity": "sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==", + "version": "11.9.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz", + "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -11016,7 +10568,6 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/express" -<<<<<<< HEAD } }, "node_modules/body-parser/node_modules/iconv-lite": { @@ -11033,17 +10584,8 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/express" -======= ->>>>>>> dev } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, "node_modules/bowser": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", @@ -11091,6 +10633,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11207,16 +10750,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001759", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", @@ -11285,16 +10818,6 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -11317,19 +10840,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -11559,6 +11069,54 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/conf": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.0.2.tgz", + "integrity": "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^10.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", @@ -11712,49 +11270,6 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -11940,18 +11455,6 @@ "node": ">= 10" } }, - "node_modules/d3-dsv/node_modules/iconv-lite": { - "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" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -12099,6 +11602,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -12278,6 +11782,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -12346,29 +11866,6 @@ "node": ">=0.10.0" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -12454,13 +11951,6 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -12474,13 +11964,6 @@ "node": ">=8" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -12549,7 +12032,6 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -12568,6 +12050,22 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-prop": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -12582,9 +12080,9 @@ } }, "node_modules/drizzle-kit": { - "version": "0.31.6", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.6.tgz", - "integrity": "sha512-/B4e/4pwnx25QwD5xXgdpo1S+077a2VZdosXbItE/oNmUgQwZydGDz9qJYmnQl/b+5IX0rLfwRhrPnroGtrg8Q==", + "version": "0.31.8", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.8.tgz", + "integrity": "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==", "dev": true, "license": "MIT", "dependencies": { @@ -13082,9 +12580,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.44.7", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz", - "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.0.tgz", + "integrity": "sha512-lyd9VRk3SXKRjV/gQckQzmJgkoYMvVG3A2JAV0vh3L+Lwk+v9+rK5Gj0H22y+ZBmxsrRBgJ5/RbQCN7DWd1dtQ==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -13224,6 +12722,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { @@ -13260,9 +12759,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.265", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.265.tgz", - "integrity": "sha512-B7IkLR1/AE+9jR2LtVF/1/6PFhY5TlnEHnlrKmGk7PvkJibg5jr+mLXLLzq3QYl6PA1T/vLDthQPqIPAlS/PPA==", + "version": "1.5.266", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", + "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -13316,60 +12815,6 @@ "node": ">=10.2.0" } }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -13503,6 +12948,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -13616,13 +13074,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -13686,12 +13137,13 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -13699,38 +13151,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" } }, "node_modules/esbuild-node-externals": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/esbuild-node-externals/-/esbuild-node-externals-1.19.1.tgz", - "integrity": "sha512-edyCKL8PLs/uamOJUkvjn2TMOdbRaQmQ9l/LOLnUEOEhzpVk/PkyiRyllKac2GX9Y+8NqGTO4DhrHT92b4nHxQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/esbuild-node-externals/-/esbuild-node-externals-1.20.1.tgz", + "integrity": "sha512-uVs+TC+PBiav2LoTz8WZT/ootINw9Rns5JJyVznlfZH1qOyZxWCPzeXklY04UtZut5qUeFFaEWtcH7XoMwiTTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13788,6 +13240,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -13843,12 +13296,12 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.3.tgz", - "integrity": "sha512-5F6qDjcZldf0Y0ZbqvWvap9xzYUxyDf7/of37aeyhvkrQokj/4bT1JYWZdlWUr283aeVa+s52mPq9ogmGg+5dw==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.8.tgz", + "integrity": "sha512-8J5cOAboXIV3f8OD6BOyj7Fik6n/as7J4MboiUSExWruf/lCu1OPR3ZVSdnta6WhzebrmAATEmNSBZsLWA6kbg==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.3", + "@next/eslint-plugin-next": "16.0.8", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -13965,6 +13418,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -14231,16 +13685,6 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -14275,18 +13719,20 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -14650,6 +14096,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -14666,6 +14113,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -14732,48 +14180,6 @@ "node": ">= 0.6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "12.23.22", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", - "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "motion-dom": "^12.23.21", - "motion-utils": "^12.23.6", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -14789,6 +14195,13 @@ "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", @@ -14976,21 +14389,15 @@ "license": "MIT" }, "node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": "20 || >=22" }, @@ -15010,13 +14417,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/glob/node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -15097,12 +14497,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "license": "MIT" - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -15190,16 +14584,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/helmet": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", @@ -15260,28 +14644,23 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/human-signals": { @@ -15303,19 +14682,15 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "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" }, "engines": { "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -15648,6 +15023,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15684,16 +15060,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -15854,19 +15220,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -15947,6 +15300,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -15958,37 +15312,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -16044,19 +15367,19 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -16241,6 +15564,153 @@ "lightningcss-win32-x64-msvc": "1.30.2" } }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "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.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "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.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "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.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "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.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "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.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "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.2", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", @@ -16262,35 +15732,67 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "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.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "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": { @@ -16374,23 +15876,6 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -16430,9 +15915,9 @@ } }, "node_modules/lucide-react": { - "version": "0.552.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz", - "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==", + "version": "0.556.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.556.0.tgz", + "integrity": "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -16492,6 +15977,29 @@ "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", @@ -16660,17 +16168,6 @@ "npm": ">=6" } }, - "node_modules/module-punycode": { - "name": "punycode", - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -16685,7 +16182,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -16696,7 +16192,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -16704,23 +16199,6 @@ "node": ">= 18" } }, - "node_modules/motion-dom": { - "version": "12.23.23", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", - "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "motion-utils": "^12.23.6" - } - }, - "node_modules/motion-utils": { - "version": "12.23.6", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", - "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -16741,18 +16219,6 @@ "url": "https://github.com/sponsors/raouldeheer" } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -16807,18 +16273,12 @@ "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, "node_modules/next": { "version": "15.5.7", "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.7", "@swc/helpers": "0.5.15", @@ -16902,58 +16362,6 @@ "integrity": "sha512-hscCKUv+5GQ0CCNbvqZ8gaxnAGToCgDTbL++jgCq8SCk/ljtZDEeQZcMk46Nm6Ynn49Q/JKF4Npo/Sq1mpbusA==", "license": "MIT" }, -<<<<<<< HEAD -======= - "node_modules/next-intl/node_modules/@swc/core": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", - "integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.3", - "@swc/core-darwin-x64": "1.15.3", - "@swc/core-linux-arm-gnueabihf": "1.15.3", - "@swc/core-linux-arm64-gnu": "1.15.3", - "@swc/core-linux-arm64-musl": "1.15.3", - "@swc/core-linux-x64-gnu": "1.15.3", - "@swc/core-linux-x64-musl": "1.15.3", - "@swc/core-win32-arm64-msvc": "1.15.3", - "@swc/core-win32-ia32-msvc": "1.15.3", - "@swc/core-win32-x64-msvc": "1.15.3" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/next-intl/node_modules/@swc/helpers": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", - "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, ->>>>>>> dev "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -17081,17 +16489,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/node-html-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", - "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -17099,9 +16496,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", - "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -17117,16 +16514,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm": { "version": "11.6.4", "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.4.tgz", @@ -18992,6 +18379,7 @@ "version": "4.0.3", "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -19118,19 +18506,6 @@ "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", "license": "MIT" }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/nypm": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", @@ -19366,53 +18741,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/oslo": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/oslo/-/oslo-1.2.1.tgz", @@ -19424,6 +18752,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", @@ -19449,6 +18797,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", @@ -19465,6 +18941,99 @@ "node": ">= 10" } }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz", + "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "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", @@ -19516,6 +19085,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -19642,6 +19212,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -19744,26 +19315,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -19833,97 +19384,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -20025,33 +19485,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -20202,16 +19635,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -20240,7 +19663,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", -<<<<<<< HEAD "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -20261,18 +19683,12 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", -======= ->>>>>>> dev "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" }, "funding": { "type": "opencollective", @@ -20308,14 +19724,15 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-day-picker": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz", - "integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.12.0.tgz", + "integrity": "sha512-t8OvG/Zrciso5CQJu5b1A7yzEmebvST+S3pOVQJWxwjjVngyG/CA2htN/D15dLI4uTEuLLkbZyS4YYt480FAtA==", "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", @@ -20338,6 +19755,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -20362,9 +19780,9 @@ } }, "node_modules/react-email": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.2.tgz", - "integrity": "sha512-WaZcnv9OAIRULY236zDRdk+8r511ooJGH5UOb7FnVsV33hGPI+l5aIZ6drVjXi4QrlLTmLm8PsYvmXRSv31MPA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.6.tgz", + "integrity": "sha512-DEGzWpEiC3CquPEaaEJuipNT3WZ9mK58rbkpOe4Slbgyf60PLa1wONnt5a3afbBBRbNdW2aYhIvVI41yS6UIRA==", "dev": true, "license": "MIT", "dependencies": { @@ -20372,6 +19790,7 @@ "@babel/traverse": "^7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", + "conf": "^15.0.2", "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", @@ -20389,7 +19808,7 @@ "email": "dist/index.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { @@ -20922,6 +20341,30 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/react-email/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/react-email/node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -20975,6 +20418,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-email/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/react-email/node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -21109,10 +20568,11 @@ } }, "node_modules/react-hook-form": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", - "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", + "version": "7.68.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", + "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -21139,21 +20599,6 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/react-promise-suspense": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", - "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^2.0.1" - } - }, - "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "license": "MIT" - }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -21254,16 +20699,6 @@ "react-dom": ">=16.6.0" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -21493,20 +20928,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -21661,63 +21082,6 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", @@ -21764,16 +21128,6 @@ "node": ">= 18" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -21845,9 +21199,9 @@ "version": "0.34.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", @@ -22116,40 +21470,6 @@ } } }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -22247,17 +21567,6 @@ "node": ">= 0.6" } }, - "node_modules/sonner": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", - "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -22288,12 +21597,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spamc": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/spamc/-/spamc-0.0.5.tgz", - "integrity": "sha512-jYXItuZuiWZyG9fIdvgTUbp2MNRuyhuSwvvhhpPJd4JK/9oSZxkD7zAj53GJtowSlXwCJzLg6sCKAoE9wXsKgg==", - "dev": true - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -22318,19 +21621,6 @@ "node": "*" } }, - "node_modules/stacktrace-parser": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", - "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -22391,6 +21681,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -22409,6 +21700,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -22423,6 +21715,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22432,12 +21725,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -22573,6 +21868,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -22585,6 +21881,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22622,18 +21919,18 @@ } }, "node_modules/stripe": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.2.1.tgz", - "integrity": "sha512-GwB1B7WSwEBzW4dilgyJruUYhbGMscrwuyHsPUmSRKrGHZ5poSh2oU9XKdii5BFVJzXHn35geRvGJ6R8bYcp8w==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.0.0.tgz", + "integrity": "sha512-EaZeWpbJOCcDytdjKSwdrL5BxzbDGNueiCfHjHXlPdBQvLqoxl6AAivC35SPzTmVXJb5duXQlXFGS45H0+e6Gg==", "license": "MIT", "dependencies": { "qs": "^6.11.0" }, "engines": { - "node": ">=12.*" + "node": ">=16" }, "peerDependencies": { - "@types/node": ">=12.x.x" + "@types/node": ">=16" }, "peerDependenciesMeta": { "@types/node": { @@ -22653,6 +21950,23 @@ ], "license": "MIT" }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -22676,39 +21990,6 @@ } } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22805,10 +22086,23 @@ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", "license": "MIT", "funding": { "type": "github", @@ -22819,7 +22113,8 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -22863,96 +22158,12 @@ "node": ">=6" } }, - "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -23053,13 +22264,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/tsc-alias": { "version": "1.8.16", "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", @@ -23196,13 +22400,13 @@ } }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -23215,490 +22419,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/tsyringe": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", @@ -23751,13 +22471,19 @@ } }, "node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", + "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", "dev": true, "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/type-is": { @@ -23853,6 +22579,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23862,15 +22589,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", - "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", + "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.1", - "@typescript-eslint/parser": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1" + "@typescript-eslint/eslint-plugin": "8.49.0", + "@typescript-eslint/parser": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -23884,6 +22611,19 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -24022,19 +22762,6 @@ } } }, - "node_modules/use-debounce": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", - "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "react": "*" - } - }, "node_modules/use-intl": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz", @@ -24149,30 +22876,6 @@ "integrity": "sha512-jHl/NQgASfw5ZML3cnbjdfr/gXK5zO8a2xKSoCVe+5+EsIaO9tMTh7SsnfhESnCpZ+Xb6XBeU91wiuyERUPshQ==", "license": "BSD-3-Clause" }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -24182,111 +22885,12 @@ "node": ">= 8" } }, - "node_modules/webpack": { - "version": "5.103.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", - "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/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/webpack/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" - } + "license": "MIT" }, "node_modules/which": { "version": "4.0.0", @@ -24390,10 +22994,11 @@ } }, "node_modules/winston": { - "version": "3.18.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", - "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -24465,6 +23070,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -24483,6 +23089,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -24500,6 +23107,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24509,12 +23117,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -24529,6 +23139,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -24541,6 +23152,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -24576,15 +23188,6 @@ } } }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -24699,10 +23302,11 @@ } }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 41ed801d..63af7f51 100644 --- a/package.json +++ b/package.json @@ -29,16 +29,17 @@ "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": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps 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" + "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs", + "format": "prettier --write ." }, "dependencies": { - "@asteasolutions/zod-to-openapi": "8.1.0", - "@aws-sdk/client-s3": "3.922.0", - "@faker-js/faker": "^10.1.0", - "@headlessui/react": "^2.2.9", + "@asteasolutions/zod-to-openapi": "8.2.0", + "@aws-sdk/client-s3": "3.947.0", + "@faker-js/faker": "10.1.0", + "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", - "@monaco-editor/react": "^4.7.0", - "@node-rs/argon2": "^2.0.2", + "@monaco-editor/react": "4.7.0", + "@node-rs/argon2": "2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@radix-ui/react-avatar": "1.1.11", @@ -49,138 +50,132 @@ "@radix-ui/react-icons": "1.3.2", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-progress": "1.1.8", "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.8", "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-tooltip": "^1.2.8", - "@react-email/components": "0.5.7", - "@react-email/render": "^1.3.2", - "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.2.2", - "@simplewebauthn/server": "^13.2.2", - "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.90.6", + "@radix-ui/react-tooltip": "1.2.8", + "@react-email/components": "1.0.1", + "@react-email/render": "2.0.0", + "@react-email/tailwind": "2.0.1", + "@simplewebauthn/browser": "13.2.2", + "@simplewebauthn/server": "13.2.2", + "@tailwindcss/forms": "0.5.10", + "@tanstack/react-query": "5.90.12", "@tanstack/react-table": "8.21.3", - "arctic": "^3.7.0", - "axios": "^1.13.2", - "better-sqlite3": "11.7.0", + "arctic": "3.7.0", + "axios": "1.13.2", + "better-sqlite3": "11.9.1", "canvas-confetti": "1.9.4", - "class-variance-authority": "^0.7.1", + "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", - "cookie": "^1.0.2", + "cookie": "1.1.1", "cookie-parser": "1.4.7", - "cookies": "^0.9.1", + "cookies": "0.9.1", "cors": "2.8.5", - "crypto-js": "^4.2.0", - "d3": "^7.9.0", + "crypto-js": "4.2.0", + "d3": "7.9.0", "date-fns": "4.1.0", - "drizzle-orm": "0.44.7", + "drizzle-orm": "0.45.0", "eslint": "9.39.1", - "eslint-config-next": "16.0.3", - "express": "5.1.0", + "eslint-config-next": "16.0.8", + "express": "5.2.1", "express-rate-limit": "8.2.1", - "glob": "11.1.0", + "glob": "13.0.0", "helmet": "8.1.0", - "http-errors": "2.0.0", - "i": "^0.3.7", + "http-errors": "2.0.1", + "i": "0.3.7", "input-otp": "1.4.2", "ioredis": "5.8.2", - "jmespath": "^0.16.0", + "jmespath": "0.16.0", "js-yaml": "4.1.1", - "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.552.0", + "jsonwebtoken": "9.0.3", + "lucide-react": "0.556.0", "maxmind": "5.0.1", "moment": "2.30.1", "next": "15.5.7", - "next-intl": "^4.4.0", + "next-intl": "4.5.8", "next-themes": "0.4.6", - "nextjs-toploader": "^3.9.17", + "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.10", - "npm": "^11.6.4", - "nprogress": "^0.2.0", + "nodemailer": "7.0.11", + "npm": "11.6.4", + "nprogress": "0.2.0", "oslo": "1.2.1", - "pg": "^8.16.2", - "posthog-node": "^5.11.2", + "pg": "8.16.3", + "posthog-node": "5.17.2", "qrcode.react": "4.2.0", "react": "19.2.1", - "react-day-picker": "9.11.1", + "react-day-picker": "9.12.0", "react-dom": "19.2.1", - "react-easy-sort": "^1.8.0", - "react-hook-form": "7.66.0", - "react-icons": "^5.5.0", + "react-easy-sort": "1.8.0", + "react-hook-form": "7.68.0", + "react-icons": "5.5.0", "rebuild": "0.1.2", - "recharts": "^2.15.4", - "reodotdev": "^1.0.0", - "resend": "^6.4.2", - "semver": "^7.7.3", - "stripe": "18.2.1", - "swagger-ui-express": "^5.0.1", - "tailwind-merge": "3.3.1", - "topojson-client": "^3.1.0", - "tw-animate-css": "^1.3.8", - "uuid": "^13.0.0", + "recharts": "2.15.4", + "reodotdev": "1.0.0", + "resend": "6.5.2", + "semver": "7.7.3", + "stripe": "20.0.0", + "swagger-ui-express": "5.0.1", + "tailwind-merge": "3.4.0", + "topojson-client": "3.1.0", + "tw-animate-css": "1.4.0", + "uuid": "13.0.0", "vaul": "1.1.2", - "visionscarto-world-atlas": "^1.0.0", - "winston": "3.18.3", + "visionscarto-world-atlas": "1.0.0", + "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", "ws": "8.18.3", - "yaml": "^2.8.1", + "yaml": "2.8.2", "yargs": "18.0.0", - "zod": "4.1.12", + "zod": "4.1.13", "zod-validation-error": "5.0.0" }, "devDependencies": { "@dotenvx/dotenvx": "1.51.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "4.3.2", - "@tailwindcss/postcss": "^4.1.17", - "@tanstack/react-query-devtools": "^5.90.2", - "@types/better-sqlite3": "7.6.12", + "@tailwindcss/postcss": "4.1.17", + "@tanstack/react-query-devtools": "5.91.1", + "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", - "@types/crypto-js": "^4.2.2", - "@types/d3": "^7.4.3", - "@types/express": "5.0.5", - "@types/express-session": "^1.18.2", - "@types/jmespath": "^0.15.2", - "@types/js-yaml": "4.0.9", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "24.10.1", - "@types/nodemailer": "7.0.3", - "@types/nprogress": "^0.2.3", + "@types/crypto-js": "4.2.2", + "@types/d3": "7.4.3", + "@types/express": "5.0.6", + "@types/express-session": "1.18.2", + "@types/jmespath": "0.15.2", + "@types/jsonwebtoken": "9.0.10", + "@types/node": "24.10.2", + "@types/nodemailer": "7.0.4", + "@types/nprogress": "0.2.3", "@types/pg": "8.15.6", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", - "@types/semver": "^7.7.1", - "@types/swagger-ui-express": "^4.1.8", - "@types/topojson-client": "^3.1.5", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@types/semver": "7.7.1", + "@types/swagger-ui-express": "4.1.8", + "@types/topojson-client": "3.1.5", "@types/ws": "8.18.1", - "@types/yargs": "17.0.34", - "babel-plugin-react-compiler": "^1.0.0", - "drizzle-kit": "0.31.6", - "esbuild": "0.27.0", - "esbuild-node-externals": "1.19.1", - "postcss": "^8", - "react-email": "4.3.2", - "tailwindcss": "^4.1.4", + "@types/yargs": "17.0.35", + "@types/js-yaml": "4.0.9", + "babel-plugin-react-compiler": "1.0.0", + "drizzle-kit": "0.31.8", + "esbuild": "0.27.1", + "esbuild-node-externals": "1.20.1", + "postcss": "8.5.6", + "prettier": "3.7.4", + "react-email": "5.0.6", + "tailwindcss": "4.1.17", "tsc-alias": "1.8.16", - "tsx": "4.20.6", - "typescript": "^5", - "typescript-eslint": "^8.46.3" - }, - "overrides": { - "emblor": { - "react": "19.0.0", - "react-dom": "19.0.0" - } + "tsx": "4.21.0", + "typescript": "5.9.3", + "typescript-eslint": "8.49.0" } -} +} \ No newline at end of file diff --git a/postcss.config.mjs b/postcss.config.mjs index 9d3299ad..19b5e42f 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,8 +1,8 @@ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { - "@tailwindcss/postcss": {}, - }, + "@tailwindcss/postcss": {} + } }; export default config; diff --git a/src/app/favicon.ico b/public/favicon.ico similarity index 100% rename from src/app/favicon.ico rename to public/favicon.ico diff --git a/server/auth/password.ts b/server/auth/password.ts index dd1a3d1b..a25af4c9 100644 --- a/server/auth/password.ts +++ b/server/auth/password.ts @@ -2,13 +2,13 @@ import { hash, verify } from "@node-rs/argon2"; export async function verifyPassword( password: string, - hash: string, + hash: string ): Promise { const validPassword = await verify(hash, password, { memoryCost: 19456, timeCost: 2, outputLen: 32, - parallelism: 1, + parallelism: 1 }); return validPassword; } @@ -18,7 +18,7 @@ export async function hashPassword(password: string): Promise { memoryCost: 19456, timeCost: 2, outputLen: 32, - parallelism: 1, + parallelism: 1 }); return passwordHash; diff --git a/server/auth/passwordSchema.ts b/server/auth/passwordSchema.ts index 9c399092..740f9a5d 100644 --- a/server/auth/passwordSchema.ts +++ b/server/auth/passwordSchema.ts @@ -4,10 +4,13 @@ export const passwordSchema = z .string() .min(8, { message: "Password must be at least 8 characters long" }) .max(128, { message: "Password must be at most 128 characters long" }) - .regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/, { - message: `Your password must meet the following conditions: + .regex( + /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/, + { + message: `Your password must meet the following conditions: at least one uppercase English letter, at least one lowercase English letter, at least one digit, at least one special character.` - }); + } + ); diff --git a/server/auth/sessions/newt.ts b/server/auth/sessions/newt.ts index 5e55c491..96c37894 100644 --- a/server/auth/sessions/newt.ts +++ b/server/auth/sessions/newt.ts @@ -1,6 +1,4 @@ -import { - encodeHexLowerCase, -} from "@oslojs/encoding"; +import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; import { Newt, newts, newtSessions, NewtSession } from "@server/db"; import { db } from "@server/db"; @@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30; export async function createNewtSession( token: string, - newtId: string, + newtId: string ): Promise { const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), + sha256(new TextEncoder().encode(token)) ); const session: NewtSession = { sessionId: sessionId, newtId, - expiresAt: new Date(Date.now() + EXPIRES).getTime(), + expiresAt: new Date(Date.now() + EXPIRES).getTime() }; await db.insert(newtSessions).values(session); return session; } export async function validateNewtSessionToken( - token: string, + token: string ): Promise { const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), + sha256(new TextEncoder().encode(token)) ); const result = await db .select({ newt: newts, session: newtSessions }) @@ -45,14 +43,12 @@ export async function validateNewtSessionToken( .where(eq(newtSessions.sessionId, session.sessionId)); return { session: null, newt: null }; } - if (Date.now() >= session.expiresAt - (EXPIRES / 2)) { - session.expiresAt = new Date( - Date.now() + EXPIRES, - ).getTime(); + if (Date.now() >= session.expiresAt - EXPIRES / 2) { + session.expiresAt = new Date(Date.now() + EXPIRES).getTime(); await db .update(newtSessions) .set({ - expiresAt: session.expiresAt, + expiresAt: session.expiresAt }) .where(eq(newtSessions.sessionId, session.sessionId)); } diff --git a/server/auth/sessions/olm.ts b/server/auth/sessions/olm.ts index 89a0e81e..a51ec79a 100644 --- a/server/auth/sessions/olm.ts +++ b/server/auth/sessions/olm.ts @@ -1,6 +1,4 @@ -import { - encodeHexLowerCase, -} from "@oslojs/encoding"; +import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; import { Olm, olms, olmSessions, OlmSession } from "@server/db"; import { db } from "@server/db"; @@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30; export async function createOlmSession( token: string, - olmId: string, + olmId: string ): Promise { const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), + sha256(new TextEncoder().encode(token)) ); const session: OlmSession = { sessionId: sessionId, olmId, - expiresAt: new Date(Date.now() + EXPIRES).getTime(), + expiresAt: new Date(Date.now() + EXPIRES).getTime() }; await db.insert(olmSessions).values(session); return session; } export async function validateOlmSessionToken( - token: string, + token: string ): Promise { const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), + sha256(new TextEncoder().encode(token)) ); const result = await db .select({ olm: olms, session: olmSessions }) @@ -45,14 +43,12 @@ export async function validateOlmSessionToken( .where(eq(olmSessions.sessionId, session.sessionId)); return { session: null, olm: null }; } - if (Date.now() >= session.expiresAt - (EXPIRES / 2)) { - session.expiresAt = new Date( - Date.now() + EXPIRES, - ).getTime(); + if (Date.now() >= session.expiresAt - EXPIRES / 2) { + session.expiresAt = new Date(Date.now() + EXPIRES).getTime(); await db .update(olmSessions) .set({ - expiresAt: session.expiresAt, + expiresAt: session.expiresAt }) .where(eq(olmSessions.sessionId, session.sessionId)); } diff --git a/server/cleanup.ts b/server/cleanup.ts index de54ed77..e494fcdc 100644 --- a/server/cleanup.ts +++ b/server/cleanup.ts @@ -1,4 +1,4 @@ -import { cleanup as wsCleanup } from "@server/routers/ws"; +import { cleanup as wsCleanup } from "#dynamic/routers/ws"; async function cleanup() { await wsCleanup(); @@ -10,4 +10,4 @@ export async function initCleanup() { // Handle process termination process.on("SIGTERM", () => cleanup()); process.on("SIGINT", () => cleanup()); -} \ No newline at end of file +} diff --git a/server/db/countries.ts b/server/db/countries.ts index 2907fd69..749f1183 100644 --- a/server/db/countries.ts +++ b/server/db/countries.ts @@ -1,1014 +1,1014 @@ export const COUNTRIES = [ { - "name": "ALL COUNTRIES", - "code": "ALL" // THIS IS AN INVALID CC SO IT WILL NEVER MATCH + name: "ALL COUNTRIES", + code: "ALL" // THIS IS AN INVALID CC SO IT WILL NEVER MATCH }, { - "name": "Afghanistan", - "code": "AF" + name: "Afghanistan", + code: "AF" }, { - "name": "Albania", - "code": "AL" + name: "Albania", + code: "AL" }, { - "name": "Algeria", - "code": "DZ" + name: "Algeria", + code: "DZ" }, { - "name": "American Samoa", - "code": "AS" + name: "American Samoa", + code: "AS" }, { - "name": "Andorra", - "code": "AD" + name: "Andorra", + code: "AD" }, { - "name": "Angola", - "code": "AO" + name: "Angola", + code: "AO" }, { - "name": "Anguilla", - "code": "AI" + name: "Anguilla", + code: "AI" }, { - "name": "Antarctica", - "code": "AQ" + name: "Antarctica", + code: "AQ" }, { - "name": "Antigua and Barbuda", - "code": "AG" + name: "Antigua and Barbuda", + code: "AG" }, { - "name": "Argentina", - "code": "AR" + name: "Argentina", + code: "AR" }, { - "name": "Armenia", - "code": "AM" + name: "Armenia", + code: "AM" }, { - "name": "Aruba", - "code": "AW" + name: "Aruba", + code: "AW" }, { - "name": "Asia/Pacific Region", - "code": "AP" + name: "Asia/Pacific Region", + code: "AP" }, { - "name": "Australia", - "code": "AU" + name: "Australia", + code: "AU" }, { - "name": "Austria", - "code": "AT" + name: "Austria", + code: "AT" }, { - "name": "Azerbaijan", - "code": "AZ" + name: "Azerbaijan", + code: "AZ" }, { - "name": "Bahamas", - "code": "BS" + name: "Bahamas", + code: "BS" }, { - "name": "Bahrain", - "code": "BH" + name: "Bahrain", + code: "BH" }, { - "name": "Bangladesh", - "code": "BD" + name: "Bangladesh", + code: "BD" }, { - "name": "Barbados", - "code": "BB" + name: "Barbados", + code: "BB" }, { - "name": "Belarus", - "code": "BY" + name: "Belarus", + code: "BY" }, { - "name": "Belgium", - "code": "BE" + name: "Belgium", + code: "BE" }, { - "name": "Belize", - "code": "BZ" + name: "Belize", + code: "BZ" }, { - "name": "Benin", - "code": "BJ" + name: "Benin", + code: "BJ" }, { - "name": "Bermuda", - "code": "BM" + name: "Bermuda", + code: "BM" }, { - "name": "Bhutan", - "code": "BT" + name: "Bhutan", + code: "BT" }, { - "name": "Bolivia", - "code": "BO" + name: "Bolivia", + code: "BO" }, { - "name": "Bonaire, Sint Eustatius and Saba", - "code": "BQ" + name: "Bonaire, Sint Eustatius and Saba", + code: "BQ" }, { - "name": "Bosnia and Herzegovina", - "code": "BA" + name: "Bosnia and Herzegovina", + code: "BA" }, { - "name": "Botswana", - "code": "BW" + name: "Botswana", + code: "BW" }, { - "name": "Bouvet Island", - "code": "BV" + name: "Bouvet Island", + code: "BV" }, { - "name": "Brazil", - "code": "BR" + name: "Brazil", + code: "BR" }, { - "name": "British Indian Ocean Territory", - "code": "IO" + name: "British Indian Ocean Territory", + code: "IO" }, { - "name": "Brunei Darussalam", - "code": "BN" + name: "Brunei Darussalam", + code: "BN" }, { - "name": "Bulgaria", - "code": "BG" + name: "Bulgaria", + code: "BG" }, { - "name": "Burkina Faso", - "code": "BF" + name: "Burkina Faso", + code: "BF" }, { - "name": "Burundi", - "code": "BI" + name: "Burundi", + code: "BI" }, { - "name": "Cambodia", - "code": "KH" + name: "Cambodia", + code: "KH" }, { - "name": "Cameroon", - "code": "CM" + name: "Cameroon", + code: "CM" }, { - "name": "Canada", - "code": "CA" + name: "Canada", + code: "CA" }, { - "name": "Cape Verde", - "code": "CV" + name: "Cape Verde", + code: "CV" }, { - "name": "Cayman Islands", - "code": "KY" + name: "Cayman Islands", + code: "KY" }, { - "name": "Central African Republic", - "code": "CF" + name: "Central African Republic", + code: "CF" }, { - "name": "Chad", - "code": "TD" + name: "Chad", + code: "TD" }, { - "name": "Chile", - "code": "CL" + name: "Chile", + code: "CL" }, { - "name": "China", - "code": "CN" + name: "China", + code: "CN" }, { - "name": "Christmas Island", - "code": "CX" + name: "Christmas Island", + code: "CX" }, { - "name": "Cocos (Keeling) Islands", - "code": "CC" + name: "Cocos (Keeling) Islands", + code: "CC" }, { - "name": "Colombia", - "code": "CO" + name: "Colombia", + code: "CO" }, { - "name": "Comoros", - "code": "KM" + name: "Comoros", + code: "KM" }, { - "name": "Congo", - "code": "CG" + name: "Congo", + code: "CG" }, { - "name": "Congo, The Democratic Republic of the", - "code": "CD" + name: "Congo, The Democratic Republic of the", + code: "CD" }, { - "name": "Cook Islands", - "code": "CK" + name: "Cook Islands", + code: "CK" }, { - "name": "Costa Rica", - "code": "CR" + name: "Costa Rica", + code: "CR" }, { - "name": "Croatia", - "code": "HR" + name: "Croatia", + code: "HR" }, { - "name": "Cuba", - "code": "CU" + name: "Cuba", + code: "CU" }, { - "name": "Curaçao", - "code": "CW" + name: "Curaçao", + code: "CW" }, { - "name": "Cyprus", - "code": "CY" + name: "Cyprus", + code: "CY" }, { - "name": "Czech Republic", - "code": "CZ" + name: "Czech Republic", + code: "CZ" }, { - "name": "Côte d'Ivoire", - "code": "CI" + name: "Côte d'Ivoire", + code: "CI" }, { - "name": "Denmark", - "code": "DK" + name: "Denmark", + code: "DK" }, { - "name": "Djibouti", - "code": "DJ" + name: "Djibouti", + code: "DJ" }, { - "name": "Dominica", - "code": "DM" + name: "Dominica", + code: "DM" }, { - "name": "Dominican Republic", - "code": "DO" + name: "Dominican Republic", + code: "DO" }, { - "name": "Ecuador", - "code": "EC" + name: "Ecuador", + code: "EC" }, { - "name": "Egypt", - "code": "EG" + name: "Egypt", + code: "EG" }, { - "name": "El Salvador", - "code": "SV" + name: "El Salvador", + code: "SV" }, { - "name": "Equatorial Guinea", - "code": "GQ" + name: "Equatorial Guinea", + code: "GQ" }, { - "name": "Eritrea", - "code": "ER" + name: "Eritrea", + code: "ER" }, { - "name": "Estonia", - "code": "EE" + name: "Estonia", + code: "EE" }, { - "name": "Ethiopia", - "code": "ET" + name: "Ethiopia", + code: "ET" }, { - "name": "Falkland Islands (Malvinas)", - "code": "FK" + name: "Falkland Islands (Malvinas)", + code: "FK" }, { - "name": "Faroe Islands", - "code": "FO" + name: "Faroe Islands", + code: "FO" }, { - "name": "Fiji", - "code": "FJ" + name: "Fiji", + code: "FJ" }, { - "name": "Finland", - "code": "FI" + name: "Finland", + code: "FI" }, { - "name": "France", - "code": "FR" + name: "France", + code: "FR" }, { - "name": "French Guiana", - "code": "GF" + name: "French Guiana", + code: "GF" }, { - "name": "French Polynesia", - "code": "PF" + name: "French Polynesia", + code: "PF" }, { - "name": "French Southern Territories", - "code": "TF" + name: "French Southern Territories", + code: "TF" }, { - "name": "Gabon", - "code": "GA" + name: "Gabon", + code: "GA" }, { - "name": "Gambia", - "code": "GM" + name: "Gambia", + code: "GM" }, { - "name": "Georgia", - "code": "GE" + name: "Georgia", + code: "GE" }, { - "name": "Germany", - "code": "DE" + name: "Germany", + code: "DE" }, { - "name": "Ghana", - "code": "GH" + name: "Ghana", + code: "GH" }, { - "name": "Gibraltar", - "code": "GI" + name: "Gibraltar", + code: "GI" }, { - "name": "Greece", - "code": "GR" + name: "Greece", + code: "GR" }, { - "name": "Greenland", - "code": "GL" + name: "Greenland", + code: "GL" }, { - "name": "Grenada", - "code": "GD" + name: "Grenada", + code: "GD" }, { - "name": "Guadeloupe", - "code": "GP" + name: "Guadeloupe", + code: "GP" }, { - "name": "Guam", - "code": "GU" + name: "Guam", + code: "GU" }, { - "name": "Guatemala", - "code": "GT" + name: "Guatemala", + code: "GT" }, { - "name": "Guernsey", - "code": "GG" + name: "Guernsey", + code: "GG" }, { - "name": "Guinea", - "code": "GN" + name: "Guinea", + code: "GN" }, { - "name": "Guinea-Bissau", - "code": "GW" + name: "Guinea-Bissau", + code: "GW" }, { - "name": "Guyana", - "code": "GY" + name: "Guyana", + code: "GY" }, { - "name": "Haiti", - "code": "HT" + name: "Haiti", + code: "HT" }, { - "name": "Heard Island and Mcdonald Islands", - "code": "HM" + name: "Heard Island and Mcdonald Islands", + code: "HM" }, { - "name": "Holy See (Vatican City State)", - "code": "VA" + name: "Holy See (Vatican City State)", + code: "VA" }, { - "name": "Honduras", - "code": "HN" + name: "Honduras", + code: "HN" }, { - "name": "Hong Kong", - "code": "HK" + name: "Hong Kong", + code: "HK" }, { - "name": "Hungary", - "code": "HU" + name: "Hungary", + code: "HU" }, { - "name": "Iceland", - "code": "IS" + name: "Iceland", + code: "IS" }, { - "name": "India", - "code": "IN" + name: "India", + code: "IN" }, { - "name": "Indonesia", - "code": "ID" + name: "Indonesia", + code: "ID" }, { - "name": "Iran, Islamic Republic Of", - "code": "IR" + name: "Iran, Islamic Republic Of", + code: "IR" }, { - "name": "Iraq", - "code": "IQ" + name: "Iraq", + code: "IQ" }, { - "name": "Ireland", - "code": "IE" + name: "Ireland", + code: "IE" }, { - "name": "Isle of Man", - "code": "IM" + name: "Isle of Man", + code: "IM" }, { - "name": "Israel", - "code": "IL" + name: "Israel", + code: "IL" }, { - "name": "Italy", - "code": "IT" + name: "Italy", + code: "IT" }, { - "name": "Jamaica", - "code": "JM" + name: "Jamaica", + code: "JM" }, { - "name": "Japan", - "code": "JP" + name: "Japan", + code: "JP" }, { - "name": "Jersey", - "code": "JE" + name: "Jersey", + code: "JE" }, { - "name": "Jordan", - "code": "JO" + name: "Jordan", + code: "JO" }, { - "name": "Kazakhstan", - "code": "KZ" + name: "Kazakhstan", + code: "KZ" }, { - "name": "Kenya", - "code": "KE" + name: "Kenya", + code: "KE" }, { - "name": "Kiribati", - "code": "KI" + name: "Kiribati", + code: "KI" }, { - "name": "Korea, Republic of", - "code": "KR" + name: "Korea, Republic of", + code: "KR" }, { - "name": "Kuwait", - "code": "KW" + name: "Kuwait", + code: "KW" }, { - "name": "Kyrgyzstan", - "code": "KG" + name: "Kyrgyzstan", + code: "KG" }, { - "name": "Laos", - "code": "LA" + name: "Laos", + code: "LA" }, { - "name": "Latvia", - "code": "LV" + name: "Latvia", + code: "LV" }, { - "name": "Lebanon", - "code": "LB" + name: "Lebanon", + code: "LB" }, { - "name": "Lesotho", - "code": "LS" + name: "Lesotho", + code: "LS" }, { - "name": "Liberia", - "code": "LR" + name: "Liberia", + code: "LR" }, { - "name": "Libyan Arab Jamahiriya", - "code": "LY" + name: "Libyan Arab Jamahiriya", + code: "LY" }, { - "name": "Liechtenstein", - "code": "LI" + name: "Liechtenstein", + code: "LI" }, { - "name": "Lithuania", - "code": "LT" + name: "Lithuania", + code: "LT" }, { - "name": "Luxembourg", - "code": "LU" + name: "Luxembourg", + code: "LU" }, { - "name": "Macao", - "code": "MO" + name: "Macao", + code: "MO" }, { - "name": "Madagascar", - "code": "MG" + name: "Madagascar", + code: "MG" }, { - "name": "Malawi", - "code": "MW" + name: "Malawi", + code: "MW" }, { - "name": "Malaysia", - "code": "MY" + name: "Malaysia", + code: "MY" }, { - "name": "Maldives", - "code": "MV" + name: "Maldives", + code: "MV" }, { - "name": "Mali", - "code": "ML" + name: "Mali", + code: "ML" }, { - "name": "Malta", - "code": "MT" + name: "Malta", + code: "MT" }, { - "name": "Marshall Islands", - "code": "MH" + name: "Marshall Islands", + code: "MH" }, { - "name": "Martinique", - "code": "MQ" + name: "Martinique", + code: "MQ" }, { - "name": "Mauritania", - "code": "MR" + name: "Mauritania", + code: "MR" }, { - "name": "Mauritius", - "code": "MU" + name: "Mauritius", + code: "MU" }, { - "name": "Mayotte", - "code": "YT" + name: "Mayotte", + code: "YT" }, { - "name": "Mexico", - "code": "MX" + name: "Mexico", + code: "MX" }, { - "name": "Micronesia, Federated States of", - "code": "FM" + name: "Micronesia, Federated States of", + code: "FM" }, { - "name": "Moldova, Republic of", - "code": "MD" + name: "Moldova, Republic of", + code: "MD" }, { - "name": "Monaco", - "code": "MC" + name: "Monaco", + code: "MC" }, { - "name": "Mongolia", - "code": "MN" + name: "Mongolia", + code: "MN" }, { - "name": "Montenegro", - "code": "ME" + name: "Montenegro", + code: "ME" }, { - "name": "Montserrat", - "code": "MS" + name: "Montserrat", + code: "MS" }, { - "name": "Morocco", - "code": "MA" + name: "Morocco", + code: "MA" }, { - "name": "Mozambique", - "code": "MZ" + name: "Mozambique", + code: "MZ" }, { - "name": "Myanmar", - "code": "MM" + name: "Myanmar", + code: "MM" }, { - "name": "Namibia", - "code": "NA" + name: "Namibia", + code: "NA" }, { - "name": "Nauru", - "code": "NR" + name: "Nauru", + code: "NR" }, { - "name": "Nepal", - "code": "NP" + name: "Nepal", + code: "NP" }, { - "name": "Netherlands", - "code": "NL" + name: "Netherlands", + code: "NL" }, { - "name": "Netherlands Antilles", - "code": "AN" + name: "Netherlands Antilles", + code: "AN" }, { - "name": "New Caledonia", - "code": "NC" + name: "New Caledonia", + code: "NC" }, { - "name": "New Zealand", - "code": "NZ" + name: "New Zealand", + code: "NZ" }, { - "name": "Nicaragua", - "code": "NI" + name: "Nicaragua", + code: "NI" }, { - "name": "Niger", - "code": "NE" + name: "Niger", + code: "NE" }, { - "name": "Nigeria", - "code": "NG" + name: "Nigeria", + code: "NG" }, { - "name": "Niue", - "code": "NU" + name: "Niue", + code: "NU" }, { - "name": "Norfolk Island", - "code": "NF" + name: "Norfolk Island", + code: "NF" }, { - "name": "North Korea", - "code": "KP" + name: "North Korea", + code: "KP" }, { - "name": "North Macedonia", - "code": "MK" + name: "North Macedonia", + code: "MK" }, { - "name": "Northern Mariana Islands", - "code": "MP" + name: "Northern Mariana Islands", + code: "MP" }, { - "name": "Norway", - "code": "NO" + name: "Norway", + code: "NO" }, { - "name": "Oman", - "code": "OM" + name: "Oman", + code: "OM" }, { - "name": "Pakistan", - "code": "PK" + name: "Pakistan", + code: "PK" }, { - "name": "Palau", - "code": "PW" + name: "Palau", + code: "PW" }, { - "name": "Palestinian Territory, Occupied", - "code": "PS" + name: "Palestinian Territory, Occupied", + code: "PS" }, { - "name": "Panama", - "code": "PA" + name: "Panama", + code: "PA" }, { - "name": "Papua New Guinea", - "code": "PG" + name: "Papua New Guinea", + code: "PG" }, { - "name": "Paraguay", - "code": "PY" + name: "Paraguay", + code: "PY" }, { - "name": "Peru", - "code": "PE" + name: "Peru", + code: "PE" }, { - "name": "Philippines", - "code": "PH" + name: "Philippines", + code: "PH" }, { - "name": "Pitcairn Islands", - "code": "PN" + name: "Pitcairn Islands", + code: "PN" }, { - "name": "Poland", - "code": "PL" + name: "Poland", + code: "PL" }, { - "name": "Portugal", - "code": "PT" + name: "Portugal", + code: "PT" }, { - "name": "Puerto Rico", - "code": "PR" + name: "Puerto Rico", + code: "PR" }, { - "name": "Qatar", - "code": "QA" + name: "Qatar", + code: "QA" }, { - "name": "Reunion", - "code": "RE" + name: "Reunion", + code: "RE" }, { - "name": "Romania", - "code": "RO" + name: "Romania", + code: "RO" }, { - "name": "Russian Federation", - "code": "RU" + name: "Russian Federation", + code: "RU" }, { - "name": "Rwanda", - "code": "RW" + name: "Rwanda", + code: "RW" }, { - "name": "Saint Barthélemy", - "code": "BL" + name: "Saint Barthélemy", + code: "BL" }, { - "name": "Saint Helena", - "code": "SH" + name: "Saint Helena", + code: "SH" }, { - "name": "Saint Kitts and Nevis", - "code": "KN" + name: "Saint Kitts and Nevis", + code: "KN" }, { - "name": "Saint Lucia", - "code": "LC" + name: "Saint Lucia", + code: "LC" }, { - "name": "Saint Martin", - "code": "MF" + name: "Saint Martin", + code: "MF" }, { - "name": "Saint Pierre and Miquelon", - "code": "PM" + name: "Saint Pierre and Miquelon", + code: "PM" }, { - "name": "Saint Vincent and the Grenadines", - "code": "VC" + name: "Saint Vincent and the Grenadines", + code: "VC" }, { - "name": "Samoa", - "code": "WS" + name: "Samoa", + code: "WS" }, { - "name": "San Marino", - "code": "SM" + name: "San Marino", + code: "SM" }, { - "name": "Sao Tome and Principe", - "code": "ST" + name: "Sao Tome and Principe", + code: "ST" }, { - "name": "Saudi Arabia", - "code": "SA" + name: "Saudi Arabia", + code: "SA" }, { - "name": "Senegal", - "code": "SN" + name: "Senegal", + code: "SN" }, { - "name": "Serbia", - "code": "RS" + name: "Serbia", + code: "RS" }, { - "name": "Serbia and Montenegro", - "code": "CS" + name: "Serbia and Montenegro", + code: "CS" }, { - "name": "Seychelles", - "code": "SC" + name: "Seychelles", + code: "SC" }, { - "name": "Sierra Leone", - "code": "SL" + name: "Sierra Leone", + code: "SL" }, { - "name": "Singapore", - "code": "SG" + name: "Singapore", + code: "SG" }, { - "name": "Sint Maarten", - "code": "SX" + name: "Sint Maarten", + code: "SX" }, { - "name": "Slovakia", - "code": "SK" + name: "Slovakia", + code: "SK" }, { - "name": "Slovenia", - "code": "SI" + name: "Slovenia", + code: "SI" }, { - "name": "Solomon Islands", - "code": "SB" + name: "Solomon Islands", + code: "SB" }, { - "name": "Somalia", - "code": "SO" + name: "Somalia", + code: "SO" }, { - "name": "South Africa", - "code": "ZA" + name: "South Africa", + code: "ZA" }, { - "name": "South Georgia and the South Sandwich Islands", - "code": "GS" + name: "South Georgia and the South Sandwich Islands", + code: "GS" }, { - "name": "South Sudan", - "code": "SS" + name: "South Sudan", + code: "SS" }, { - "name": "Spain", - "code": "ES" + name: "Spain", + code: "ES" }, { - "name": "Sri Lanka", - "code": "LK" + name: "Sri Lanka", + code: "LK" }, { - "name": "Sudan", - "code": "SD" + name: "Sudan", + code: "SD" }, { - "name": "Suriname", - "code": "SR" + name: "Suriname", + code: "SR" }, { - "name": "Svalbard and Jan Mayen", - "code": "SJ" + name: "Svalbard and Jan Mayen", + code: "SJ" }, { - "name": "Swaziland", - "code": "SZ" + name: "Swaziland", + code: "SZ" }, { - "name": "Sweden", - "code": "SE" + name: "Sweden", + code: "SE" }, { - "name": "Switzerland", - "code": "CH" + name: "Switzerland", + code: "CH" }, { - "name": "Syrian Arab Republic", - "code": "SY" + name: "Syrian Arab Republic", + code: "SY" }, { - "name": "Taiwan", - "code": "TW" + name: "Taiwan", + code: "TW" }, { - "name": "Tajikistan", - "code": "TJ" + name: "Tajikistan", + code: "TJ" }, { - "name": "Tanzania, United Republic of", - "code": "TZ" + name: "Tanzania, United Republic of", + code: "TZ" }, { - "name": "Thailand", - "code": "TH" + name: "Thailand", + code: "TH" }, { - "name": "Timor-Leste", - "code": "TL" + name: "Timor-Leste", + code: "TL" }, { - "name": "Togo", - "code": "TG" + name: "Togo", + code: "TG" }, { - "name": "Tokelau", - "code": "TK" + name: "Tokelau", + code: "TK" }, { - "name": "Tonga", - "code": "TO" + name: "Tonga", + code: "TO" }, { - "name": "Trinidad and Tobago", - "code": "TT" + name: "Trinidad and Tobago", + code: "TT" }, { - "name": "Tunisia", - "code": "TN" + name: "Tunisia", + code: "TN" }, { - "name": "Turkey", - "code": "TR" + name: "Turkey", + code: "TR" }, { - "name": "Turkmenistan", - "code": "TM" + name: "Turkmenistan", + code: "TM" }, { - "name": "Turks and Caicos Islands", - "code": "TC" + name: "Turks and Caicos Islands", + code: "TC" }, { - "name": "Tuvalu", - "code": "TV" + name: "Tuvalu", + code: "TV" }, { - "name": "Uganda", - "code": "UG" + name: "Uganda", + code: "UG" }, { - "name": "Ukraine", - "code": "UA" + name: "Ukraine", + code: "UA" }, { - "name": "United Arab Emirates", - "code": "AE" + name: "United Arab Emirates", + code: "AE" }, { - "name": "United Kingdom", - "code": "GB" + name: "United Kingdom", + code: "GB" }, { - "name": "United States", - "code": "US" + name: "United States", + code: "US" }, { - "name": "United States Minor Outlying Islands", - "code": "UM" + name: "United States Minor Outlying Islands", + code: "UM" }, { - "name": "Uruguay", - "code": "UY" + name: "Uruguay", + code: "UY" }, { - "name": "Uzbekistan", - "code": "UZ" + name: "Uzbekistan", + code: "UZ" }, { - "name": "Vanuatu", - "code": "VU" + name: "Vanuatu", + code: "VU" }, { - "name": "Venezuela", - "code": "VE" + name: "Venezuela", + code: "VE" }, { - "name": "Vietnam", - "code": "VN" + name: "Vietnam", + code: "VN" }, { - "name": "Virgin Islands, British", - "code": "VG" + name: "Virgin Islands, British", + code: "VG" }, { - "name": "Virgin Islands, U.S.", - "code": "VI" + name: "Virgin Islands, U.S.", + code: "VI" }, { - "name": "Wallis and Futuna", - "code": "WF" + name: "Wallis and Futuna", + code: "WF" }, { - "name": "Western Sahara", - "code": "EH" + name: "Western Sahara", + code: "EH" }, { - "name": "Yemen", - "code": "YE" + name: "Yemen", + code: "YE" }, { - "name": "Zambia", - "code": "ZM" + name: "Zambia", + code: "ZM" }, { - "name": "Zimbabwe", - "code": "ZW" + name: "Zimbabwe", + code: "ZW" }, { - "name": "Åland Islands", - "code": "AX" + name: "Åland Islands", + code: "AX" } -]; \ No newline at end of file +]; diff --git a/server/db/names.json b/server/db/names.json index fdf545fb..eb104691 100644 --- a/server/db/names.json +++ b/server/db/names.json @@ -1708,4 +1708,4 @@ "Desert Box Turtle", "African Striped Weasel" ] -} \ No newline at end of file +} diff --git a/server/db/names.ts b/server/db/names.ts index a3da96e7..32b0a393 100644 --- a/server/db/names.ts +++ b/server/db/names.ts @@ -1,6 +1,7 @@ import { join } from "path"; import { readFileSync } from "fs"; -import { db, resources, siteResources } from "@server/db"; +import { clients, db, resources, siteResources } from "@server/db"; +import { randomInt } from "crypto"; import { exitNodes, sites } from "@server/db"; import { eq, and } from "drizzle-orm"; import { __DIRNAME } from "@server/lib/consts"; @@ -15,6 +16,25 @@ if (!dev) { } export const names = JSON.parse(readFileSync(file, "utf-8")); +export async function getUniqueClientName(orgId: string): Promise { + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + const name = generateName(); + const count = await db + .select({ niceId: clients.niceId, orgId: clients.orgId }) + .from(clients) + .where(and(eq(clients.niceId, name), eq(clients.orgId, orgId))); + if (count.length === 0) { + return name; + } + loops++; + } +} + export async function getUniqueSiteName(orgId: string): Promise { let loops = 0; while (true) { @@ -46,11 +66,21 @@ export async function getUniqueResourceName(orgId: string): Promise { db .select({ niceId: resources.niceId, orgId: resources.orgId }) .from(resources) - .where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))), + .where( + and(eq(resources.niceId, name), eq(resources.orgId, orgId)) + ), db - .select({ niceId: siteResources.niceId, orgId: siteResources.orgId }) + .select({ + niceId: siteResources.niceId, + orgId: siteResources.orgId + }) .from(siteResources) - .where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId))) + .where( + and( + eq(siteResources.niceId, name), + eq(siteResources.orgId, orgId) + ) + ) ]); if (resourceCount.length === 0 && siteResourceCount.length === 0) { return name; @@ -59,7 +89,9 @@ export async function getUniqueResourceName(orgId: string): Promise { } } -export async function getUniqueSiteResourceName(orgId: string): Promise { +export async function getUniqueSiteResourceName( + orgId: string +): Promise { let loops = 0; while (true) { if (loops > 100) { @@ -71,11 +103,21 @@ export async function getUniqueSiteResourceName(orgId: string): Promise db .select({ niceId: resources.niceId, orgId: resources.orgId }) .from(resources) - .where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))), + .where( + and(eq(resources.niceId, name), eq(resources.orgId, orgId)) + ), db - .select({ niceId: siteResources.niceId, orgId: siteResources.orgId }) + .select({ + niceId: siteResources.niceId, + orgId: siteResources.orgId + }) .from(siteResources) - .where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId))) + .where( + and( + eq(siteResources.niceId, name), + eq(siteResources.orgId, orgId) + ) + ) ]); if (resourceCount.length === 0 && siteResourceCount.length === 0) { return name; @@ -86,9 +128,7 @@ export async function getUniqueSiteResourceName(orgId: string): Promise export async function getUniqueExitNodeEndpointName(): Promise { let loops = 0; - const count = await db - .select() - .from(exitNodes); + const count = await db.select().from(exitNodes); while (true) { if (loops > 100) { throw new Error("Could not generate a unique name"); @@ -107,14 +147,11 @@ export async function getUniqueExitNodeEndpointName(): Promise { } } - export function generateName(): string { const name = ( - names.descriptors[ - Math.floor(Math.random() * names.descriptors.length) - ] + + names.descriptors[randomInt(names.descriptors.length)] + "-" + - names.animals[Math.floor(Math.random() * names.animals.length)] + names.animals[randomInt(names.animals.length)] ) .toLowerCase() .replace(/\s/g, "-"); diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 35378961..9456effb 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -51,7 +51,7 @@ function createDb() { if (!replicaConnections.length) { replicas.push( DrizzlePostgres(primaryPool, { - logger: process.env.NODE_ENV === "development" + logger: process.env.QUERY_LOGGING == "true" }) ); } else { @@ -65,7 +65,7 @@ function createDb() { }); replicas.push( DrizzlePostgres(replicaPool, { - logger: process.env.NODE_ENV === "development" + logger: process.env.QUERY_LOGGING == "true" }) ); } @@ -73,7 +73,7 @@ function createDb() { return withReplicas( DrizzlePostgres(primaryPool, { - logger: process.env.QUERY_LOGGING === "true" + logger: process.env.QUERY_LOGGING == "true" }), replicas as any ); diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 17d262c6..cb809b71 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -215,42 +215,56 @@ export const sessionTransferToken = pgTable("sessionTransferToken", { expiresAt: bigint("expiresAt", { mode: "number" }).notNull() }); -export const actionAuditLog = pgTable("actionAuditLog", { - id: serial("id").primaryKey(), - timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: varchar("actorType", { length: 50 }).notNull(), - actor: varchar("actor", { length: 255 }).notNull(), - actorId: varchar("actorId", { length: 255 }).notNull(), - action: varchar("action", { length: 100 }).notNull(), - metadata: text("metadata") -}, (table) => ([ - index("idx_actionAuditLog_timestamp").on(table.timestamp), - index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const actionAuditLog = pgTable( + "actionAuditLog", + { + id: serial("id").primaryKey(), + timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: varchar("actorType", { length: 50 }).notNull(), + actor: varchar("actor", { length: 255 }).notNull(), + actorId: varchar("actorId", { length: 255 }).notNull(), + action: varchar("action", { length: 100 }).notNull(), + metadata: text("metadata") + }, + (table) => [ + index("idx_actionAuditLog_timestamp").on(table.timestamp), + index("idx_actionAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); -export const accessAuditLog = pgTable("accessAuditLog", { - id: serial("id").primaryKey(), - timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: varchar("actorType", { length: 50 }), - actor: varchar("actor", { length: 255 }), - actorId: varchar("actorId", { length: 255 }), - resourceId: integer("resourceId"), - ip: varchar("ip", { length: 45 }), - type: varchar("type", { length: 100 }).notNull(), - action: boolean("action").notNull(), - location: text("location"), - userAgent: text("userAgent"), - metadata: text("metadata") -}, (table) => ([ - index("idx_identityAuditLog_timestamp").on(table.timestamp), - index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const accessAuditLog = pgTable( + "accessAuditLog", + { + id: serial("id").primaryKey(), + timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: varchar("actorType", { length: 50 }), + actor: varchar("actor", { length: 255 }), + actorId: varchar("actorId", { length: 255 }), + resourceId: integer("resourceId"), + ip: varchar("ip", { length: 45 }), + type: varchar("type", { length: 100 }).notNull(), + action: boolean("action").notNull(), + location: text("location"), + userAgent: text("userAgent"), + metadata: text("metadata") + }, + (table) => [ + index("idx_identityAuditLog_timestamp").on(table.timestamp), + index("idx_identityAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -270,4 +284,4 @@ export type RemoteExitNodeSession = InferSelectModel< export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; export type ActionAuditLog = InferSelectModel; -export type AccessAuditLog = InferSelectModel; \ No newline at end of file +export type AccessAuditLog = InferSelectModel; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index f59f50a4..71877f2f 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -46,13 +46,13 @@ export const orgs = pgTable("orgs", { requireTwoFactor: boolean("requireTwoFactor"), maxSessionLengthHours: integer("maxSessionLengthHours"), passwordExpiryDays: integer("passwordExpiryDays"), - settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever + settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever, and 9001 = end of the following year .notNull() .default(7), - settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") + settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year .notNull() .default(0), - settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") + settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year .notNull() .default(0) }); @@ -177,7 +177,7 @@ export const targetHealthCheck = pgTable("targetHealthCheck", { hcMethod: varchar("hcMethod").default("GET"), hcStatus: integer("hcStatus"), // http code hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" - hcTlsServerName: text("hcTlsServerName"), + hcTlsServerName: text("hcTlsServerName") }); export const exitNodes = pgTable("exitNodes", { @@ -644,6 +644,7 @@ export const clients = pgTable("clients", { // optionally tied to a user and in this case delete when the user deletes onDelete: "cascade" }), + niceId: varchar("niceId").notNull(), olmId: text("olmId"), // to lock it to a specific olm optionally name: varchar("name").notNull(), pubKey: varchar("pubKey"), diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 85bd7cc7..774c4e53 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -52,10 +52,7 @@ export async function getResourceByDomain( resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) - .innerJoin( - orgs, - eq(orgs.orgId, resources.orgId) - ) + .innerJoin(orgs, eq(orgs.orgId, resources.orgId)) .where(eq(resources.fullDomain, domain)) .limit(1); diff --git a/server/db/sqlite/migrate.ts b/server/db/sqlite/migrate.ts index e4a730d0..7c337ae2 100644 --- a/server/db/sqlite/migrate.ts +++ b/server/db/sqlite/migrate.ts @@ -8,7 +8,7 @@ const runMigrations = async () => { console.log("Running migrations..."); try { migrate(db as any, { - migrationsFolder: migrationsFolder, + migrationsFolder: migrationsFolder }); console.log("Migrations completed successfully."); } catch (error) { diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 65396770..975a949b 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -29,7 +29,9 @@ export const certificates = sqliteTable("certificates", { }); export const dnsChallenge = sqliteTable("dnsChallenges", { - dnsChallengeId: integer("dnsChallengeId").primaryKey({ autoIncrement: true }), + dnsChallengeId: integer("dnsChallengeId").primaryKey({ + autoIncrement: true + }), domain: text("domain").notNull(), token: text("token").notNull(), keyAuthorization: text("keyAuthorization").notNull(), @@ -61,9 +63,7 @@ export const customers = sqliteTable("customers", { }); export const subscriptions = sqliteTable("subscriptions", { - subscriptionId: text("subscriptionId") - .primaryKey() - .notNull(), + subscriptionId: text("subscriptionId").primaryKey().notNull(), customerId: text("customerId") .notNull() .references(() => customers.customerId, { onDelete: "cascade" }), @@ -75,7 +75,9 @@ export const subscriptions = sqliteTable("subscriptions", { }); export const subscriptionItems = sqliteTable("subscriptionItems", { - subscriptionItemId: integer("subscriptionItemId").primaryKey({ autoIncrement: true }), + subscriptionItemId: integer("subscriptionItemId").primaryKey({ + autoIncrement: true + }), subscriptionId: text("subscriptionId") .notNull() .references(() => subscriptions.subscriptionId, { @@ -129,7 +131,9 @@ export const limits = sqliteTable("limits", { }); export const usageNotifications = sqliteTable("usageNotifications", { - notificationId: integer("notificationId").primaryKey({ autoIncrement: true }), + notificationId: integer("notificationId").primaryKey({ + autoIncrement: true + }), orgId: text("orgId") .notNull() .references(() => orgs.orgId, { onDelete: "cascade" }), @@ -210,42 +214,56 @@ export const sessionTransferToken = sqliteTable("sessionTransferToken", { expiresAt: integer("expiresAt").notNull() }); -export const actionAuditLog = sqliteTable("actionAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), - timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: text("actorType").notNull(), - actor: text("actor").notNull(), - actorId: text("actorId").notNull(), - action: text("action").notNull(), - metadata: text("metadata") -}, (table) => ([ - index("idx_actionAuditLog_timestamp").on(table.timestamp), - index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const actionAuditLog = sqliteTable( + "actionAuditLog", + { + id: integer("id").primaryKey({ autoIncrement: true }), + timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: text("actorType").notNull(), + actor: text("actor").notNull(), + actorId: text("actorId").notNull(), + action: text("action").notNull(), + metadata: text("metadata") + }, + (table) => [ + index("idx_actionAuditLog_timestamp").on(table.timestamp), + index("idx_actionAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); -export const accessAuditLog = sqliteTable("accessAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), - timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: text("actorType"), - actor: text("actor"), - actorId: text("actorId"), - resourceId: integer("resourceId"), - ip: text("ip"), - location: text("location"), - type: text("type").notNull(), - action: integer("action", { mode: "boolean" }).notNull(), - userAgent: text("userAgent"), - metadata: text("metadata") -}, (table) => ([ - index("idx_identityAuditLog_timestamp").on(table.timestamp), - index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const accessAuditLog = sqliteTable( + "accessAuditLog", + { + id: integer("id").primaryKey({ autoIncrement: true }), + timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: text("actorType"), + actor: text("actor"), + actorId: text("actorId"), + resourceId: integer("resourceId"), + ip: text("ip"), + location: text("location"), + type: text("type").notNull(), + action: integer("action", { mode: "boolean" }).notNull(), + userAgent: text("userAgent"), + metadata: text("metadata") + }, + (table) => [ + index("idx_identityAuditLog_timestamp").on(table.timestamp), + index("idx_identityAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -265,4 +283,4 @@ export type RemoteExitNodeSession = InferSelectModel< export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; export type ActionAuditLog = InferSelectModel; -export type AccessAuditLog = InferSelectModel; \ No newline at end of file +export type AccessAuditLog = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index e9500141..6e17cac4 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -38,13 +38,13 @@ export const orgs = sqliteTable("orgs", { requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }), maxSessionLengthHours: integer("maxSessionLengthHours"), // hours passwordExpiryDays: integer("passwordExpiryDays"), // days - settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever + settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year .notNull() .default(7), - settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") + settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year .notNull() .default(0), - settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") + settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year .notNull() .default(0) }); @@ -196,7 +196,7 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { hcMethod: text("hcMethod").default("GET"), hcStatus: integer("hcStatus"), // http code hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" - hcTlsServerName: text("hcTlsServerName"), + hcTlsServerName: text("hcTlsServerName") }); export const exitNodes = sqliteTable("exitNodes", { @@ -350,7 +350,7 @@ export const clients = sqliteTable("clients", { // optionally tied to a user and in this case delete when the user deletes onDelete: "cascade" }), - + niceId: text("niceId").notNull(), name: text("name").notNull(), pubKey: text("pubKey"), olmId: text("olmId"), // to lock it to a specific olm optionally diff --git a/server/emails/index.ts b/server/emails/index.ts index 42cfa39c..01cc6610 100644 --- a/server/emails/index.ts +++ b/server/emails/index.ts @@ -18,10 +18,13 @@ function createEmailClient() { host: emailConfig.smtp_host, port: emailConfig.smtp_port, secure: emailConfig.smtp_secure || false, - auth: (emailConfig.smtp_user && emailConfig.smtp_pass) ? { - user: emailConfig.smtp_user, - pass: emailConfig.smtp_pass - } : null + auth: + emailConfig.smtp_user && emailConfig.smtp_pass + ? { + user: emailConfig.smtp_user, + pass: emailConfig.smtp_pass + } + : null } as SMTPTransport.Options; if (emailConfig.smtp_tls_reject_unauthorized !== undefined) { diff --git a/server/emails/templates/NotifyUsageLimitApproaching.tsx b/server/emails/templates/NotifyUsageLimitApproaching.tsx index beab0300..161b3676 100644 --- a/server/emails/templates/NotifyUsageLimitApproaching.tsx +++ b/server/emails/templates/NotifyUsageLimitApproaching.tsx @@ -19,7 +19,13 @@ interface Props { billingLink: string; // Link to billing page } -export const NotifyUsageLimitApproaching = ({ email, limitName, currentUsage, usageLimit, billingLink }: Props) => { +export const NotifyUsageLimitApproaching = ({ + email, + limitName, + currentUsage, + usageLimit, + billingLink +}: Props) => { const previewText = `Your usage for ${limitName} is approaching the limit.`; const usagePercentage = Math.round((currentUsage / usageLimit) * 100); @@ -37,23 +43,32 @@ export const NotifyUsageLimitApproaching = ({ email, limitName, currentUsage, us Hi there, - We wanted to let you know that your usage for {limitName} is approaching your plan limit. + We wanted to let you know that your usage for{" "} + {limitName} is approaching your + plan limit. - Current Usage: {currentUsage} of {usageLimit} ({usagePercentage}%) + Current Usage: {currentUsage} of{" "} + {usageLimit} ({usagePercentage}%) - Once you reach your limit, some functionality may be restricted or your sites may disconnect until you upgrade your plan or your usage resets. + Once you reach your limit, some functionality may be + restricted or your sites may disconnect until you + upgrade your plan or your usage resets. - To avoid any interruption to your service, we recommend upgrading your plan or monitoring your usage closely. You can upgrade your plan here. + To avoid any interruption to your service, we + recommend upgrading your plan or monitoring your + usage closely. You can{" "} + upgrade your plan here. - If you have any questions or need assistance, please don't hesitate to reach out to our support team. + If you have any questions or need assistance, please + don't hesitate to reach out to our support team. diff --git a/server/emails/templates/NotifyUsageLimitReached.tsx b/server/emails/templates/NotifyUsageLimitReached.tsx index 783d1b0e..59841670 100644 --- a/server/emails/templates/NotifyUsageLimitReached.tsx +++ b/server/emails/templates/NotifyUsageLimitReached.tsx @@ -19,7 +19,13 @@ interface Props { billingLink: string; // Link to billing page } -export const NotifyUsageLimitReached = ({ email, limitName, currentUsage, usageLimit, billingLink }: Props) => { +export const NotifyUsageLimitReached = ({ + email, + limitName, + currentUsage, + usageLimit, + billingLink +}: Props) => { const previewText = `You've reached your ${limitName} usage limit - Action required`; const usagePercentage = Math.round((currentUsage / usageLimit) * 100); @@ -32,30 +38,48 @@ export const NotifyUsageLimitReached = ({ email, limitName, currentUsage, usageL - Usage Limit Reached - Action Required + + Usage Limit Reached - Action Required + Hi there, - You have reached your usage limit for {limitName}. + You have reached your usage limit for{" "} + {limitName}. - Current Usage: {currentUsage} of {usageLimit} ({usagePercentage}%) + Current Usage: {currentUsage} of{" "} + {usageLimit} ({usagePercentage}%) - Important: Your functionality may now be restricted and your sites may disconnect until you either upgrade your plan or your usage resets. To prevent any service interruption, immediate action is recommended. + Important: Your functionality may + now be restricted and your sites may disconnect + until you either upgrade your plan or your usage + resets. To prevent any service interruption, + immediate action is recommended. What you can do: -
Upgrade your plan immediately to restore full functionality -
• Monitor your usage to stay within limits in the future +
•{" "} + + Upgrade your plan immediately + {" "} + to restore full functionality +
• Monitor your usage to stay within limits in + the future
- If you have any questions or need immediate assistance, please contact our support team right away. + If you have any questions or need immediate + assistance, please contact our support team right + away. diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts index 3416004c..0ef0c0af 100644 --- a/server/integrationApiServer.ts +++ b/server/integrationApiServer.ts @@ -5,7 +5,7 @@ import config from "@server/lib/config"; import logger from "@server/logger"; import { errorHandlerMiddleware, - notFoundMiddleware, + notFoundMiddleware } from "@server/middlewares"; import { authenticated, unauthenticated } from "#dynamic/routers/integration"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; diff --git a/server/lib/billing/features.ts b/server/lib/billing/features.ts index b72543cc..d074894a 100644 --- a/server/lib/billing/features.ts +++ b/server/lib/billing/features.ts @@ -25,16 +25,22 @@ export const FeatureMeterIdsSandbox: Record = { }; export function getFeatureMeterId(featureId: FeatureId): string { - if (process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") { + if ( + process.env.ENVIRONMENT == "prod" && + process.env.SANDBOX_MODE !== "true" + ) { return FeatureMeterIds[featureId]; } else { return FeatureMeterIdsSandbox[featureId]; } } -export function getFeatureIdByMetricId(metricId: string): FeatureId | undefined { - return (Object.entries(FeatureMeterIds) as [FeatureId, string][]) - .find(([_, v]) => v === metricId)?.[0]; +export function getFeatureIdByMetricId( + metricId: string +): FeatureId | undefined { + return (Object.entries(FeatureMeterIds) as [FeatureId, string][]).find( + ([_, v]) => v === metricId + )?.[0]; } export type FeaturePriceSet = { @@ -43,7 +49,8 @@ export type FeaturePriceSet = { [FeatureId.DOMAINS]?: string; // Optional since domains are not billed }; -export const standardFeaturePriceSet: FeaturePriceSet = { // Free tier matches the freeLimitSet +export const standardFeaturePriceSet: FeaturePriceSet = { + // Free tier matches the freeLimitSet [FeatureId.SITE_UPTIME]: "price_1RrQc4D3Ee2Ir7WmaJGZ3MtF", [FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea", [FeatureId.EGRESS_DATA_MB]: "price_1RrQXFD3Ee2Ir7WmvGDlgxQk", @@ -51,7 +58,8 @@ export const standardFeaturePriceSet: FeaturePriceSet = { // Free tier matches t [FeatureId.REMOTE_EXIT_NODES]: "price_1S46weD3Ee2Ir7Wm94KEHI4h" }; -export const standardFeaturePriceSetSandbox: FeaturePriceSet = { // Free tier matches the freeLimitSet +export const standardFeaturePriceSetSandbox: FeaturePriceSet = { + // Free tier matches the freeLimitSet [FeatureId.SITE_UPTIME]: "price_1RefFBDCpkOb237BPrKZ8IEU", [FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF", [FeatureId.EGRESS_DATA_MB]: "price_1Rfp9LDCpkOb237BwuN5Oiu0", @@ -60,15 +68,20 @@ export const standardFeaturePriceSetSandbox: FeaturePriceSet = { // Free tier ma }; export function getStandardFeaturePriceSet(): FeaturePriceSet { - if (process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") { + if ( + process.env.ENVIRONMENT == "prod" && + process.env.SANDBOX_MODE !== "true" + ) { return standardFeaturePriceSet; } else { return standardFeaturePriceSetSandbox; } } -export function getLineItems(featurePriceSet: FeaturePriceSet): Stripe.Checkout.SessionCreateParams.LineItem[] { +export function getLineItems( + featurePriceSet: FeaturePriceSet +): Stripe.Checkout.SessionCreateParams.LineItem[] { return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({ - price: priceId, + price: priceId })); -} \ No newline at end of file +} diff --git a/server/lib/billing/index.ts b/server/lib/billing/index.ts index 6c3ef792..54c9ee2e 100644 --- a/server/lib/billing/index.ts +++ b/server/lib/billing/index.ts @@ -2,4 +2,4 @@ export * from "./limitSet"; export * from "./features"; export * from "./limitsService"; export * from "./getOrgTierData"; -export * from "./createCustomer"; \ No newline at end of file +export * from "./createCustomer"; diff --git a/server/lib/billing/limitSet.ts b/server/lib/billing/limitSet.ts index 153d8ae8..820b121a 100644 --- a/server/lib/billing/limitSet.ts +++ b/server/lib/billing/limitSet.ts @@ -12,7 +12,7 @@ export const sandboxLimitSet: LimitSet = { [FeatureId.USERS]: { value: 1, description: "Sandbox limit" }, [FeatureId.EGRESS_DATA_MB]: { value: 1000, description: "Sandbox limit" }, // 1 GB [FeatureId.DOMAINS]: { value: 0, description: "Sandbox limit" }, - [FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" }, + [FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" } }; export const freeLimitSet: LimitSet = { @@ -29,7 +29,7 @@ export const freeLimitSet: LimitSet = { export const subscribedLimitSet: LimitSet = { [FeatureId.SITE_UPTIME]: { value: 2232000, - description: "Contact us to increase soft limit.", + description: "Contact us to increase soft limit." }, // 50 sites up for 31 days [FeatureId.USERS]: { value: 150, @@ -38,7 +38,7 @@ export const subscribedLimitSet: LimitSet = { [FeatureId.EGRESS_DATA_MB]: { value: 12000000, description: "Contact us to increase soft limit." - }, // 12000 GB + }, // 12000 GB [FeatureId.DOMAINS]: { value: 25, description: "Contact us to increase soft limit." diff --git a/server/lib/billing/tiers.ts b/server/lib/billing/tiers.ts index 6ccf8898..ae49a48f 100644 --- a/server/lib/billing/tiers.ts +++ b/server/lib/billing/tiers.ts @@ -1,22 +1,32 @@ export enum TierId { - STANDARD = "standard", + STANDARD = "standard" } export type TierPriceSet = { [key in TierId]: string; }; -export const tierPriceSet: TierPriceSet = { // Free tier matches the freeLimitSet - [TierId.STANDARD]: "price_1RrQ9cD3Ee2Ir7Wmqdy3KBa0", +export const tierPriceSet: TierPriceSet = { + // Free tier matches the freeLimitSet + [TierId.STANDARD]: "price_1RrQ9cD3Ee2Ir7Wmqdy3KBa0" }; -export const tierPriceSetSandbox: TierPriceSet = { // Free tier matches the freeLimitSet +export const tierPriceSetSandbox: TierPriceSet = { + // Free tier matches the freeLimitSet // when matching tier the keys closer to 0 index are matched first so list the tiers in descending order of value - [TierId.STANDARD]: "price_1RrAYJDCpkOb237By2s1P32m", + [TierId.STANDARD]: "price_1RrAYJDCpkOb237By2s1P32m" }; -export function getTierPriceSet(environment?: string, sandbox_mode?: boolean): TierPriceSet { - if ((process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") || (environment === "prod" && sandbox_mode !== true)) { // THIS GETS LOADED CLIENT SIDE AND SERVER SIDE +export function getTierPriceSet( + environment?: string, + sandbox_mode?: boolean +): TierPriceSet { + if ( + (process.env.ENVIRONMENT == "prod" && + process.env.SANDBOX_MODE !== "true") || + (environment === "prod" && sandbox_mode !== true) + ) { + // THIS GETS LOADED CLIENT SIDE AND SERVER SIDE return tierPriceSet; } else { return tierPriceSetSandbox; diff --git a/server/lib/billing/usageService.ts b/server/lib/billing/usageService.ts index 8e6f5e9c..0fde8eba 100644 --- a/server/lib/billing/usageService.ts +++ b/server/lib/billing/usageService.ts @@ -19,7 +19,7 @@ import logger from "@server/logger"; import { sendToClient } from "#dynamic/routers/ws"; import { build } from "@server/build"; import { s3Client } from "@server/lib/s3"; -import cache from "@server/lib/cache"; +import cache from "@server/lib/cache"; interface StripeEvent { identifier?: string; diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index 7b4f3b22..6168f85d 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -14,6 +14,7 @@ import { import { BlueprintSource } from "@server/routers/blueprints/types"; import { stringify as stringifyYaml } from "yaml"; import { faker } from "@faker-js/faker"; +import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource"; type ApplyBlueprintArgs = { orgId: string; @@ -57,22 +58,63 @@ export async function applyBlueprint({ trx, siteId ); - }); - logger.debug( - `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}` - ); + logger.debug( + `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}` + ); - // We need to update the targets on the newts from the successfully updated information - for (const result of proxyResourcesResults) { - for (const target of result.targetsToUpdate) { - const [site] = await db + // We need to update the targets on the newts from the successfully updated information + for (const result of proxyResourcesResults) { + for (const target of result.targetsToUpdate) { + const [site] = await trx + .select() + .from(sites) + .innerJoin(newts, eq(sites.siteId, newts.siteId)) + .where( + and( + eq(sites.siteId, target.siteId), + eq(sites.orgId, orgId), + eq(sites.type, "newt"), + isNotNull(sites.pubKey) + ) + ) + .limit(1); + + if (site) { + logger.debug( + `Updating target ${target.targetId} on site ${site.sites.siteId}` + ); + + // see if you can find a matching target health check from the healthchecksToUpdate array + const matchingHealthcheck = + result.healthchecksToUpdate.find( + (hc) => hc.targetId === target.targetId + ); + + await addProxyTargets( + site.newt.newtId, + [target], + matchingHealthcheck ? [matchingHealthcheck] : [], + result.proxyResource.protocol, + result.proxyResource.proxyPort + ); + } + } + } + + logger.debug( + `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}` + ); + + // We need to update the targets on the newts from the successfully updated information + for (const result of clientResourcesResults) { + const [site] = await trx .select() .from(sites) .innerJoin(newts, eq(sites.siteId, newts.siteId)) .where( and( - eq(sites.siteId, target.siteId), + eq(sites.siteId, result.newSiteResource.siteId), eq(sites.orgId, orgId), eq(sites.type, "newt"), isNotNull(sites.pubKey) @@ -80,60 +122,33 @@ export async function applyBlueprint({ ) .limit(1); - if (site) { + if (!site) { logger.debug( - `Updating target ${target.targetId} on site ${site.sites.siteId}` - ); - - // see if you can find a matching target health check from the healthchecksToUpdate array - const matchingHealthcheck = - result.healthchecksToUpdate.find( - (hc) => hc.targetId === target.targetId - ); - - await addProxyTargets( - site.newt.newtId, - [target], - matchingHealthcheck ? [matchingHealthcheck] : [], - result.proxyResource.protocol, - result.proxyResource.proxyPort + `No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update` ); + continue; } + + logger.debug( + `Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}` + ); + + await handleMessagingForUpdatedSiteResource( + result.oldSiteResource, + result.newSiteResource, + { siteId: site.sites.siteId, orgId: site.sites.orgId }, + trx + ); + + // await addClientTargets( + // site.newt.newtId, + // result.resource.destination, + // result.resource.destinationPort, + // result.resource.protocol, + // result.resource.proxyPort + // ); } - } - - logger.debug( - `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}` - ); - - // We need to update the targets on the newts from the successfully updated information - for (const result of clientResourcesResults) { - const [site] = await db - .select() - .from(sites) - .innerJoin(newts, eq(sites.siteId, newts.siteId)) - .where( - and( - eq(sites.siteId, result.resource.siteId), - eq(sites.orgId, orgId), - eq(sites.type, "newt"), - isNotNull(sites.pubKey) - ) - ) - .limit(1); - - logger.debug( - `Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}` - ); - - // await addClientTargets( - // site.newt.newtId, - // result.resource.destination, - // result.resource.destinationPort, - // result.resource.protocol, - // result.resource.proxyPort - // ); - } + }); blueprintSucceeded = true; blueprintMessage = "Blueprint applied successfully"; @@ -171,53 +186,3 @@ export async function applyBlueprint({ return blueprint; } - -// await updateDatabaseFromConfig("org_i21aifypnlyxur2", { -// resources: { -// "resource-nice-id": { -// name: "this is my resource", -// protocol: "http", -// "full-domain": "level1.test.example.com", -// "host-header": "example.com", -// "tls-server-name": "example.com", -// auth: { -// pincode: 123456, -// password: "sadfasdfadsf", -// "sso-enabled": true, -// "sso-roles": ["Member"], -// "sso-users": ["owen@pangolin.net"], -// "whitelist-users": ["owen@pangolin.net"] -// }, -// targets: [ -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// method: "http", -// port: 8000, -// healthcheck: { -// port: 8000, -// hostname: "localhost" -// } -// }, -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// method: "http", -// port: 8001 -// } -// ] -// }, -// "resource-nice-id2": { -// name: "http server", -// protocol: "tcp", -// "proxy-port": 3000, -// targets: [ -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// port: 3000, -// } -// ] -// } -// } -// }); diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts index 0fe7c3fe..f27cc05b 100644 --- a/server/lib/blueprints/applyNewtDockerBlueprint.ts +++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts @@ -34,7 +34,10 @@ export async function applyNewtDockerBlueprint( return; } - if (isEmptyObject(blueprint["proxy-resources"]) && isEmptyObject(blueprint["client-resources"])) { + if ( + isEmptyObject(blueprint["proxy-resources"]) && + isEmptyObject(blueprint["client-resources"]) + ) { return; } diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts index 7b92ba21..ab65336d 100644 --- a/server/lib/blueprints/clientResources.ts +++ b/server/lib/blueprints/clientResources.ts @@ -1,17 +1,23 @@ import { + clients, + clientSiteResources, + roles, + roleSiteResources, SiteResource, siteResources, Transaction, + userOrgs, + users, + userSiteResources } from "@server/db"; import { sites } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import { - Config, -} from "./types"; +import { eq, and, ne, inArray } from "drizzle-orm"; +import { Config } from "./types"; import logger from "@server/logger"; export type ClientResourcesResults = { - resource: SiteResource; + newSiteResource: SiteResource; + oldSiteResource?: SiteResource; }[]; export async function updateClientResources( @@ -69,17 +75,22 @@ export async function updateClientResources( } if (existingResource) { + if (existingResource.siteId !== site.siteId) { + throw new Error( + `You can not change the site of an existing client resource (${resourceNiceId}). Please delete and recreate it instead.` + ); + } + // Update existing resource const [updatedResource] = await trx .update(siteResources) .set({ name: resourceData.name || resourceNiceId, - siteId: site.siteId, - mode: "port", - proxyPort: resourceData["proxy-port"]!, - destination: resourceData.hostname, - destinationPort: resourceData["internal-port"], - protocol: resourceData.protocol + mode: resourceData.mode, + destination: resourceData.destination, + enabled: true, // hardcoded for now + // enabled: resourceData.enabled ?? true, + alias: resourceData.alias || null }) .where( eq( @@ -89,7 +100,110 @@ export async function updateClientResources( ) .returning(); - results.push({ resource: updatedResource }); + const siteResourceId = existingResource.siteResourceId; + const orgId = existingResource.orgId; + + await trx + .delete(clientSiteResources) + .where(eq(clientSiteResources.siteResourceId, siteResourceId)); + + if (resourceData.machines.length > 0) { + // get clientIds from niceIds + const clientsToUpdate = await trx + .select() + .from(clients) + .where( + and( + inArray(clients.niceId, resourceData.machines), + eq(clients.orgId, orgId) + ) + ); + + const clientIds = clientsToUpdate.map( + (client) => client.clientId + ); + + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } + + await trx + .delete(userSiteResources) + .where(eq(userSiteResources.siteResourceId, siteResourceId)); + + if (resourceData.users.length > 0) { + // get userIds from username + const usersToUpdate = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where( + and( + inArray(users.username, resourceData.users), + eq(userOrgs.orgId, orgId) + ) + ); + + const userIds = usersToUpdate.map((user) => user.user.userId); + + await trx + .insert(userSiteResources) + .values( + userIds.map((userId) => ({ userId, siteResourceId })) + ); + } + + // Get all admin role IDs for this org to exclude from deletion + const adminRoles = await trx + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))); + const adminRoleIds = adminRoles.map((role) => role.roleId); + + if (adminRoleIds.length > 0) { + await trx.delete(roleSiteResources).where( + and( + eq(roleSiteResources.siteResourceId, siteResourceId), + ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role + ) + ); + } else { + await trx + .delete(roleSiteResources) + .where( + eq(roleSiteResources.siteResourceId, siteResourceId) + ); + } + + if (resourceData.roles.length > 0) { + // Re-add specified roles but we need to get the roleIds from the role name in the array + const rolesToUpdate = await trx + .select() + .from(roles) + .where( + and( + eq(roles.orgId, orgId), + inArray(roles.name, resourceData.roles) + ) + ); + + const roleIds = rolesToUpdate.map((role) => role.roleId); + + await trx + .insert(roleSiteResources) + .values( + roleIds.map((roleId) => ({ roleId, siteResourceId })) + ); + } + + results.push({ + newSiteResource: updatedResource, + oldSiteResource: existingResource + }); } else { // Create new resource const [newResource] = await trx @@ -99,19 +213,103 @@ export async function updateClientResources( siteId: site.siteId, niceId: resourceNiceId, name: resourceData.name || resourceNiceId, - mode: "port", - proxyPort: resourceData["proxy-port"]!, - destination: resourceData.hostname, - destinationPort: resourceData["internal-port"], - protocol: resourceData.protocol + mode: resourceData.mode, + destination: resourceData.destination, + enabled: true, // hardcoded for now + // enabled: resourceData.enabled ?? true, + alias: resourceData.alias || null }) .returning(); + const siteResourceId = newResource.siteResourceId; + + const [adminRole] = await trx + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (!adminRole) { + throw new Error(`Admin role not found for org ${orgId}`); + } + + await trx.insert(roleSiteResources).values({ + roleId: adminRole.roleId, + siteResourceId: siteResourceId + }); + + if (resourceData.roles.length > 0) { + // get roleIds from role names + const rolesToUpdate = await trx + .select() + .from(roles) + .where( + and( + eq(roles.orgId, orgId), + inArray(roles.name, resourceData.roles) + ) + ); + + const roleIds = rolesToUpdate.map((role) => role.roleId); + + await trx + .insert(roleSiteResources) + .values( + roleIds.map((roleId) => ({ roleId, siteResourceId })) + ); + } + + if (resourceData.users.length > 0) { + // get userIds from username + const usersToUpdate = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where( + and( + inArray(users.username, resourceData.users), + eq(userOrgs.orgId, orgId) + ) + ); + + const userIds = usersToUpdate.map((user) => user.user.userId); + + await trx + .insert(userSiteResources) + .values( + userIds.map((userId) => ({ userId, siteResourceId })) + ); + } + + if (resourceData.machines.length > 0) { + // get clientIds from niceIds + const clientsToUpdate = await trx + .select() + .from(clients) + .where( + and( + inArray(clients.niceId, resourceData.machines), + eq(clients.orgId, orgId) + ) + ); + + const clientIds = clientsToUpdate.map( + (client) => client.clientId + ); + + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } + logger.info( `Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}` ); - results.push({ resource: newResource }); + results.push({ newSiteResource: newResource }); } } diff --git a/server/lib/blueprints/parseDockerContainers.ts b/server/lib/blueprints/parseDockerContainers.ts index 1510e6e1..f2cdcfa2 100644 --- a/server/lib/blueprints/parseDockerContainers.ts +++ b/server/lib/blueprints/parseDockerContainers.ts @@ -84,12 +84,20 @@ export function processContainerLabels(containers: Container[]): { // Process proxy resources if (Object.keys(proxyResourceLabels).length > 0) { - processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]); + processResourceLabels( + proxyResourceLabels, + container, + result["proxy-resources"] + ); } // Process client resources if (Object.keys(clientResourceLabels).length > 0) { - processResourceLabels(clientResourceLabels, container, result["client-resources"]); + processResourceLabels( + clientResourceLabels, + container, + result["client-resources"] + ); } }); @@ -161,8 +169,7 @@ function processResourceLabels( const finalTarget = { ...target }; if (!finalTarget.hostname) { finalTarget.hostname = - container.name || - container.hostname; + container.name || container.hostname; } if (!finalTarget.port) { const containerPort = diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 9761f57d..706fab12 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -221,7 +221,8 @@ export async function updateProxyResources( domainId: domain ? domain.domainId : null, enabled: resourceEnabled, sso: resourceData.auth?.["sso-enabled"] || false, - skipToIdpId: resourceData.auth?.["auto-login-idp"] || null, + skipToIdpId: + resourceData.auth?.["auto-login-idp"] || null, ssl: resourceSsl, setHostHeader: resourceData["host-header"] || null, tlsServerName: resourceData["tls-server-name"] || null, @@ -546,7 +547,8 @@ export async function updateProxyResources( if ( existingRule.action !== getRuleAction(rule.action) || existingRule.match !== rule.match.toUpperCase() || - existingRule.value !== getRuleValue(rule.match.toUpperCase(), rule.value) + existingRule.value !== + getRuleValue(rule.match.toUpperCase(), rule.value) ) { validateRule(rule); await trx @@ -554,7 +556,10 @@ export async function updateProxyResources( .set({ action: getRuleAction(rule.action), match: rule.match.toUpperCase(), - value: getRuleValue(rule.match.toUpperCase(), rule.value), + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ) }) .where( eq(resourceRules.ruleId, existingRule.ruleId) @@ -566,7 +571,10 @@ export async function updateProxyResources( resourceId: existingResource.resourceId, action: getRuleAction(rule.action), match: rule.match.toUpperCase(), - value: getRuleValue(rule.match.toUpperCase(), rule.value), + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ), priority: index + 1 // start priorities at 1 }); } @@ -852,16 +860,16 @@ async function syncUserResources( .from(userResources) .where(eq(userResources.resourceId, resourceId)); - for (const email of ssoUsers) { + for (const username of ssoUsers) { const [user] = await trx .select() .from(users) .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) - .where(and(eq(users.email, email), eq(userOrgs.orgId, orgId))) + .where(and(eq(users.username, username), eq(userOrgs.orgId, orgId))) .limit(1); if (!user) { - throw new Error(`User not found: ${email} in org ${orgId}`); + throw new Error(`User not found: ${username} in org ${orgId}`); } const existingUserResource = existingUserResources.find( @@ -889,7 +897,11 @@ async function syncUserResources( ) .limit(1); - if (user && user.user.email && !ssoUsers.includes(user.user.email)) { + if ( + user && + user.user.username && + !ssoUsers.includes(user.user.username) + ) { await trx .delete(userResources) .where( @@ -1074,10 +1086,8 @@ async function getDomainId( // remove the base domain of the domain let subdomain = null; - if (domainSelection.type == "ns") { - if (fullDomain != baseDomain) { - subdomain = fullDomain.replace(`.${baseDomain}`, ""); - } + if (fullDomain != baseDomain) { + subdomain = fullDomain.replace(`.${baseDomain}`, ""); } // Return the first valid domain diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 75a0ae17..23e2176f 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -9,14 +9,18 @@ export const TargetHealthCheckSchema = z.object({ hostname: z.string(), port: z.int().min(1).max(65535), enabled: z.boolean().optional().default(true), - path: z.string().optional(), + path: z.string().optional().default("/"), scheme: z.string().optional(), mode: z.string().default("http"), interval: z.int().default(30), "unhealthy-interval": z.int().default(30), unhealthyInterval: z.int().optional(), // deprecated alias timeout: z.int().default(5), - headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable() + .optional() + .default(null), "follow-redirects": z.boolean().default(true), followRedirects: z.boolean().optional(), // deprecated alias method: z.string().default("GET"), @@ -36,7 +40,10 @@ export const TargetSchema = z.object({ healthcheck: TargetHealthCheckSchema.optional(), rewritePath: z.string().optional(), // deprecated alias "rewrite-path": z.string().optional(), - "rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), + "rewrite-match": z + .enum(["exact", "prefix", "regex", "stripPrefix"]) + .optional() + .nullable(), priority: z.int().min(1).max(1000).optional().default(100) }); export type TargetData = z.infer; @@ -45,10 +52,12 @@ export const AuthSchema = z.object({ // pincode has to have 6 digits pincode: z.number().min(100000).max(999999).optional(), password: z.string().min(1).optional(), - "basic-auth": z.object({ - user: z.string().min(1), - password: z.string().min(1) - }).optional(), + "basic-auth": z + .object({ + user: z.string().min(1), + password: z.string().min(1) + }) + .optional(), "sso-enabled": z.boolean().optional().default(false), "sso-roles": z .array(z.string()) @@ -59,7 +68,7 @@ export const AuthSchema = z.object({ }), "sso-users": z.array(z.email()).optional().default([]), "whitelist-users": z.array(z.email()).optional().default([]), - "auto-login-idp": z.int().positive().optional(), + "auto-login-idp": z.int().positive().optional() }); export const RuleSchema = z.object({ @@ -122,7 +131,6 @@ export const ResourceSchema = z { path: ["targets"], error: "When protocol is 'http', all targets must have a 'method' field" - } ) .refine( @@ -204,130 +212,222 @@ export function isTargetsOnlyResource(resource: any): boolean { return Object.keys(resource).length === 1 && resource.targets; } -export const ClientResourceSchema = z.object({ - name: z.string().min(2).max(100), - site: z.string().min(2).max(100).optional(), - protocol: z.enum(["tcp", "udp"]), - "proxy-port": z.number().min(1).max(65535), - "hostname": z.string().min(1).max(255), - "internal-port": z.number().min(1).max(65535), - enabled: z.boolean().optional().default(true) -}); +export const ClientResourceSchema = z + .object({ + name: z.string().min(1).max(255), + mode: z.enum(["host", "cidr"]), + site: z.string(), + // protocol: z.enum(["tcp", "udp"]).optional(), + // proxyPort: z.int().positive().optional(), + // destinationPort: z.int().positive().optional(), + destination: z.string().min(1), + // enabled: z.boolean().default(true), + alias: z + .string() + .regex( + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name (e.g., example.com)" + ) + .optional(), + roles: z + .array(z.string()) + .optional() + .default([]) + .refine((roles) => !roles.includes("Admin"), { + error: "Admin role cannot be included in roles" + }), + users: z.array(z.email()).optional().default([]), + machines: z.array(z.string()).optional().default([]) + }) + .refine( + (data) => { + if (data.mode === "host") { + // Check if it's a valid IP address using zod (v4 or v6) + const isValidIP = z + .union([z.ipv4(), z.ipv6()]) + .safeParse(data.destination).success; + + if (isValidIP) { + return true; + } + + // Check if it's a valid domain (hostname pattern, TLD not required) + const domainRegex = + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; + const isValidDomain = domainRegex.test(data.destination); + const isValidAlias = data.alias && domainRegex.test(data.alias); + + return isValidDomain && isValidAlias; // require the alias to be set in the case of domain + } + return true; + }, + { + message: + "Destination must be a valid IP address or valid domain AND alias is required" + } + ) + .refine( + (data) => { + if (data.mode === "cidr") { + // Check if it's a valid CIDR (v4 or v6) + const isValidCIDR = z + .union([z.cidrv4(), z.cidrv6()]) + .safeParse(data.destination).success; + return isValidCIDR; + } + return true; + }, + { + message: "Destination must be a valid CIDR notation for cidr mode" + } + ); // Schema for the entire configuration object export const ConfigSchema = z .object({ - "proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}), - "client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}), + "proxy-resources": z + .record(z.string(), ResourceSchema) + .optional() + .prefault({}), + "public-resources": z + .record(z.string(), ResourceSchema) + .optional() + .prefault({}), + "client-resources": z + .record(z.string(), ClientResourceSchema) + .optional() + .prefault({}), + "private-resources": z + .record(z.string(), ClientResourceSchema) + .optional() + .prefault({}), sites: z.record(z.string(), SiteSchema).optional().prefault({}) }) - .refine( + .transform((data) => { + // Merge public-resources into proxy-resources + if (data["public-resources"]) { + data["proxy-resources"] = { + ...data["proxy-resources"], + ...data["public-resources"] + }; + delete (data as any)["public-resources"]; + } + + // Merge private-resources into client-resources + if (data["private-resources"]) { + data["client-resources"] = { + ...data["client-resources"], + ...data["private-resources"] + }; + delete (data as any)["private-resources"]; + } + + return data as { + "proxy-resources": Record>; + "client-resources": Record< + string, + z.infer + >; + sites: Record>; + }; + }) + .superRefine((config, ctx) => { // Enforce the full-domain uniqueness across resources in the same stack - (config) => { - // Extract duplicates for error message - const fullDomainMap = new Map(); + const fullDomainMap = new Map(); - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const fullDomain = resource["full-domain"]; - if (fullDomain) { - // Only process if full-domain is defined - if (!fullDomainMap.has(fullDomain)) { - fullDomainMap.set(fullDomain, []); - } - fullDomainMap.get(fullDomain)!.push(resourceKey); + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const fullDomain = resource["full-domain"]; + if (fullDomain) { + // Only process if full-domain is defined + if (!fullDomainMap.has(fullDomain)) { + fullDomainMap.set(fullDomain, []); } + fullDomainMap.get(fullDomain)!.push(resourceKey); } - ); - - const duplicates = Array.from(fullDomainMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([fullDomain, resourceKeys]) => - `'${fullDomain}' used by resources: ${resourceKeys.join(", ")}` - ) - .join("; "); - - if (duplicates.length !== 0) { - return { - path: ["resources"], - error: `Duplicate 'full-domain' values found: ${duplicates}` - }; } + ); + + const fullDomainDuplicates = Array.from(fullDomainMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map( + ([fullDomain, resourceKeys]) => + `'${fullDomain}' used by resources: ${resourceKeys.join(", ")}` + ) + .join("; "); + + if (fullDomainDuplicates.length !== 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["proxy-resources"], + message: `Duplicate 'full-domain' values found: ${fullDomainDuplicates}` + }); } - ) - .refine( + // Enforce proxy-port uniqueness within proxy-resources per protocol - (config) => { - // Extract duplicates for error message - const protocolPortMap = new Map(); + const protocolPortMap = new Map(); - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - const protocol = resource.protocol; - if (proxyPort !== undefined && protocol !== undefined) { - const key = `${protocol}:${proxyPort}`; - if (!protocolPortMap.has(key)) { - protocolPortMap.set(key, []); - } - protocolPortMap.get(key)!.push(resourceKey); + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const proxyPort = resource["proxy-port"]; + const protocol = resource.protocol; + if (proxyPort !== undefined && protocol !== undefined) { + const key = `${protocol}:${proxyPort}`; + if (!protocolPortMap.has(key)) { + protocolPortMap.set(key, []); } + protocolPortMap.get(key)!.push(resourceKey); } - ); - - const duplicates = Array.from(protocolPortMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([protocolPort, resourceKeys]) => { - const [protocol, port] = protocolPort.split(':'); - return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`; - } - ) - .join("; "); - - if (duplicates.length !== 0) { - return { - path: ["proxy-resources"], - error: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}` - }; } - } - ) - .refine( - // Enforce proxy-port uniqueness within client-resources - (config) => { - // Extract duplicates for error message - const proxyPortMap = new Map(); + ); - Object.entries(config["client-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - if (proxyPort !== undefined) { - if (!proxyPortMap.has(proxyPort)) { - proxyPortMap.set(proxyPort, []); - } - proxyPortMap.get(proxyPort)!.push(resourceKey); + const portDuplicates = Array.from(protocolPortMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map(([protocolPort, resourceKeys]) => { + const [protocol, port] = protocolPort.split(":"); + return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`; + }) + .join("; "); + + if (portDuplicates.length !== 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["proxy-resources"], + message: `Duplicate 'proxy-port' values found in proxy-resources: ${portDuplicates}` + }); + } + + // Enforce alias uniqueness within client-resources + const aliasMap = new Map(); + + Object.entries(config["client-resources"]).forEach( + ([resourceKey, resource]) => { + const alias = resource.alias; + if (alias !== undefined) { + if (!aliasMap.has(alias)) { + aliasMap.set(alias, []); } + aliasMap.get(alias)!.push(resourceKey); } - ); - - const duplicates = Array.from(proxyPortMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([proxyPort, resourceKeys]) => - `port ${proxyPort} used by client-resources: ${resourceKeys.join(", ")}` - ) - .join("; "); - - if (duplicates.length !== 0) { - return { - path: ["client-resources"], - error: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}` - }; } + ); + + const aliasDuplicates = Array.from(aliasMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map( + ([alias, resourceKeys]) => + `alias '${alias}' used by client-resources: ${resourceKeys.join(", ")}` + ) + .join("; "); + + if (aliasDuplicates.length !== 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["client-resources"], + message: `Duplicate 'alias' values found in client-resources: ${aliasDuplicates}` + }); } - ); + }); // Type inference from the schema export type Site = z.infer; diff --git a/server/lib/cache.ts b/server/lib/cache.ts index efa7d201..82c80280 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -2,4 +2,4 @@ import NodeCache from "node-cache"; export const cache = new NodeCache({ stdTTL: 3600, checkperiod: 120 }); -export default cache; \ No newline at end of file +export default cache; diff --git a/server/lib/calculateUserClientsForOrgs.ts b/server/lib/calculateUserClientsForOrgs.ts index 4cde8657..ac3d719f 100644 --- a/server/lib/calculateUserClientsForOrgs.ts +++ b/server/lib/calculateUserClientsForOrgs.ts @@ -15,6 +15,7 @@ import { getNextAvailableClientSubnet } from "@server/lib/ip"; import logger from "@server/logger"; import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations"; import { sendTerminateClient } from "@server/routers/client/terminate"; +import { getUniqueClientName } from "@server/db/names"; export async function calculateUserClientsForOrgs( userId: string, @@ -165,7 +166,10 @@ export async function calculateUserClientsForOrgs( ]; // Get next available subnet - const newSubnet = await getNextAvailableClientSubnet(orgId); + const newSubnet = await getNextAvailableClientSubnet( + orgId, + transaction + ); if (!newSubnet) { logger.warn( `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no available subnet found` @@ -176,6 +180,8 @@ export async function calculateUserClientsForOrgs( const subnet = newSubnet.split("/")[0]; const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; + const niceId = await getUniqueClientName(orgId); + // Create the client const [newClient] = await transaction .insert(clients) @@ -186,7 +192,8 @@ export async function calculateUserClientsForOrgs( name: olm.name || "User Client", subnet: updatedSubnet, olmId: olm.olmId, - type: "olm" + type: "olm", + niceId }) .returning(); diff --git a/server/lib/certificates.ts b/server/lib/certificates.ts index a6c51c96..f5860ff3 100644 --- a/server/lib/certificates.ts +++ b/server/lib/certificates.ts @@ -1,4 +1,6 @@ -export async function getValidCertificatesForDomains(domains: Set): Promise< +export async function getValidCertificatesForDomains( + domains: Set +): Promise< Array<{ id: number; domain: string; @@ -10,4 +12,4 @@ export async function getValidCertificatesForDomains(domains: Set): Prom }> > { return []; // stub -} \ No newline at end of file +} diff --git a/server/lib/cleanupLogs.test.ts b/server/lib/cleanupLogs.test.ts new file mode 100644 index 00000000..dc9326e1 --- /dev/null +++ b/server/lib/cleanupLogs.test.ts @@ -0,0 +1,270 @@ +import { assertEquals } from "@test/assert"; + +// Helper to create a timestamp from a date string (UTC) +function dateToTimestamp(dateStr: string): number { + return Math.floor(new Date(dateStr).getTime() / 1000); +} + +// Testable version of calculateCutoffTimestamp that accepts a "now" timestamp +// This matches the logic in cleanupLogs.ts but allows injecting the current time +function calculateCutoffTimestampWithNow( + retentionDays: number, + nowTimestamp: number +): number { + if (retentionDays === 9001) { + // Special case: data is erased at the end of the year following the year it was generated + // This means we delete logs from 2 years ago or older (logs from year Y are deleted after Dec 31 of year Y+1) + const currentYear = new Date(nowTimestamp * 1000).getUTCFullYear(); + // Cutoff is the start of the year before last (Jan 1, currentYear - 1 at 00:00:00) + // Any logs before this date are from 2+ years ago and should be deleted + const cutoffDate = new Date(Date.UTC(currentYear - 1, 0, 1, 0, 0, 0)); + return Math.floor(cutoffDate.getTime() / 1000); + } else { + return nowTimestamp - retentionDays * 24 * 60 * 60; + } +} + +function testCalculateCutoffTimestamp() { + console.log("Running calculateCutoffTimestamp tests..."); + + // Test 1: Normal retention days (e.g., 30 days) + { + const now = dateToTimestamp("2025-12-06T12:00:00Z"); + const result = calculateCutoffTimestampWithNow(30, now); + const expected = now - 30 * 24 * 60 * 60; + assertEquals(result, expected, "30 days retention calculation failed"); + } + + // Test 2: Normal retention days (e.g., 90 days) + { + const now = dateToTimestamp("2025-06-15T00:00:00Z"); + const result = calculateCutoffTimestampWithNow(90, now); + const expected = now - 90 * 24 * 60 * 60; + assertEquals(result, expected, "90 days retention calculation failed"); + } + + // Test 3: Special case 9001 - December 2025 (before Dec 31) + // Data from 2024 should NOT be deleted yet (must wait until after Dec 31, 2025) + // Data from 2023 and earlier should be deleted + // Cutoff should be Jan 1, 2024 (start of currentYear - 1) + { + const now = dateToTimestamp("2025-12-06T12:00:00Z"); + const result = calculateCutoffTimestampWithNow(9001, now); + const expected = dateToTimestamp("2024-01-01T00:00:00Z"); + assertEquals( + result, + expected, + "9001 retention (Dec 2025) - should cutoff at Jan 1, 2024" + ); + } + + // Test 4: Special case 9001 - January 2026 + // Data from 2024 should now be deleted (Dec 31, 2025 has passed) + // Cutoff should be Jan 1, 2025 (start of currentYear - 1) + { + const now = dateToTimestamp("2026-01-15T12:00:00Z"); + const result = calculateCutoffTimestampWithNow(9001, now); + const expected = dateToTimestamp("2025-01-01T00:00:00Z"); + assertEquals( + result, + expected, + "9001 retention (Jan 2026) - should cutoff at Jan 1, 2025" + ); + } + + // Test 5: Special case 9001 - December 31, 2025 at 23:59:59 UTC + // Still in 2025, so data from 2024 should NOT be deleted yet + // Cutoff should be Jan 1, 2024 + { + const now = dateToTimestamp("2025-12-31T23:59:59Z"); + const result = calculateCutoffTimestampWithNow(9001, now); + const expected = dateToTimestamp("2024-01-01T00:00:00Z"); + assertEquals( + result, + expected, + "9001 retention (Dec 31, 2025 23:59:59) - should cutoff at Jan 1, 2024" + ); + } + + // Test 6: Special case 9001 - January 1, 2026 at 00:00:01 UTC + // Now in 2026, so data from 2024 should be deleted + // Cutoff should be Jan 1, 2025 + { + const now = dateToTimestamp("2026-01-01T00:00:01Z"); + const result = calculateCutoffTimestampWithNow(9001, now); + const expected = dateToTimestamp("2025-01-01T00:00:00Z"); + assertEquals( + result, + expected, + "9001 retention (Jan 1, 2026 00:00:01) - should cutoff at Jan 1, 2025" + ); + } + + // Test 7: Special case 9001 - Mid year 2025 + // Cutoff should still be Jan 1, 2024 + { + const now = dateToTimestamp("2025-06-15T12:00:00Z"); + const result = calculateCutoffTimestampWithNow(9001, now); + const expected = dateToTimestamp("2024-01-01T00:00:00Z"); + assertEquals( + result, + expected, + "9001 retention (mid 2025) - should cutoff at Jan 1, 2024" + ); + } + + // Test 8: Special case 9001 - Early 2024 + // Cutoff should be Jan 1, 2023 + { + const now = dateToTimestamp("2024-02-01T12:00:00Z"); + const result = calculateCutoffTimestampWithNow(9001, now); + const expected = dateToTimestamp("2023-01-01T00:00:00Z"); + assertEquals( + result, + expected, + "9001 retention (early 2024) - should cutoff at Jan 1, 2023" + ); + } + + // Test 9: 1 day retention + { + const now = dateToTimestamp("2025-12-06T12:00:00Z"); + const result = calculateCutoffTimestampWithNow(1, now); + const expected = now - 1 * 24 * 60 * 60; + assertEquals(result, expected, "1 day retention calculation failed"); + } + + // Test 10: 365 days retention (1 year) + { + const now = dateToTimestamp("2025-12-06T12:00:00Z"); + const result = calculateCutoffTimestampWithNow(365, now); + const expected = now - 365 * 24 * 60 * 60; + assertEquals(result, expected, "365 days retention calculation failed"); + } + + // Test 11: Verify 9001 deletes logs correctly across year boundary + // If we're in 2025, logs from Dec 31, 2023 (timestamp) should be DELETED (before cutoff) + // But logs from Jan 1, 2024 (timestamp) should be KEPT (at or after cutoff) + { + const now = dateToTimestamp("2025-12-06T12:00:00Z"); + const cutoff = calculateCutoffTimestampWithNow(9001, now); + const logFromDec2023 = dateToTimestamp("2023-12-31T23:59:59Z"); + const logFromJan2024 = dateToTimestamp("2024-01-01T00:00:00Z"); + + // Log from Dec 2023 should be before cutoff (deleted) + assertEquals( + logFromDec2023 < cutoff, + true, + "Log from Dec 2023 should be deleted" + ); + // Log from Jan 2024 should be at or after cutoff (kept) + assertEquals( + logFromJan2024 >= cutoff, + true, + "Log from Jan 2024 should be kept" + ); + } + + // Test 12: Verify 9001 in 2026 - logs from 2024 should now be deleted + { + const now = dateToTimestamp("2026-03-15T12:00:00Z"); + const cutoff = calculateCutoffTimestampWithNow(9001, now); + const logFromDec2024 = dateToTimestamp("2024-12-31T23:59:59Z"); + const logFromJan2025 = dateToTimestamp("2025-01-01T00:00:00Z"); + + // Log from Dec 2024 should be before cutoff (deleted) + assertEquals( + logFromDec2024 < cutoff, + true, + "Log from Dec 2024 should be deleted in 2026" + ); + // Log from Jan 2025 should be at or after cutoff (kept) + assertEquals( + logFromJan2025 >= cutoff, + true, + "Log from Jan 2025 should be kept in 2026" + ); + } + + // Test 13: Edge case - exactly at year boundary for 9001 + // On Jan 1, 2025 00:00:00 UTC, cutoff should be Jan 1, 2024 + { + const now = dateToTimestamp("2025-01-01T00:00:00Z"); + const result = calculateCutoffTimestampWithNow(9001, now); + const expected = dateToTimestamp("2024-01-01T00:00:00Z"); + assertEquals( + result, + expected, + "9001 retention (Jan 1, 2025 00:00:00) - should cutoff at Jan 1, 2024" + ); + } + + // Test 14: Verify data from 2024 is kept throughout 2025 when using 9001 + // Example: Log created on July 15, 2024 should be kept until Dec 31, 2025 + { + // Running in June 2025 + const nowJune2025 = dateToTimestamp("2025-06-15T12:00:00Z"); + const cutoffJune2025 = calculateCutoffTimestampWithNow( + 9001, + nowJune2025 + ); + const logFromJuly2024 = dateToTimestamp("2024-07-15T12:00:00Z"); + + // Log from July 2024 should be KEPT in June 2025 + assertEquals( + logFromJuly2024 >= cutoffJune2025, + true, + "Log from July 2024 should be kept in June 2025" + ); + + // Running in January 2026 + const nowJan2026 = dateToTimestamp("2026-01-15T12:00:00Z"); + const cutoffJan2026 = calculateCutoffTimestampWithNow(9001, nowJan2026); + + // Log from July 2024 should be DELETED in January 2026 + assertEquals( + logFromJuly2024 < cutoffJan2026, + true, + "Log from July 2024 should be deleted in Jan 2026" + ); + } + + // Test 15: Verify the exact requirement - data from 2024 must be purged on December 31, 2025 + // On Dec 31, 2025 (still 2025), data from 2024 should still exist + // On Jan 1, 2026 (now 2026), data from 2024 can be deleted + { + const logFromMid2024 = dateToTimestamp("2024-06-15T12:00:00Z"); + + // Dec 31, 2025 23:59:59 - still 2025, log should be kept + const nowDec31_2025 = dateToTimestamp("2025-12-31T23:59:59Z"); + const cutoffDec31 = calculateCutoffTimestampWithNow( + 9001, + nowDec31_2025 + ); + assertEquals( + logFromMid2024 >= cutoffDec31, + true, + "Log from mid-2024 should be kept on Dec 31, 2025" + ); + + // Jan 1, 2026 00:00:00 - now 2026, log can be deleted + const nowJan1_2026 = dateToTimestamp("2026-01-01T00:00:00Z"); + const cutoffJan1 = calculateCutoffTimestampWithNow(9001, nowJan1_2026); + assertEquals( + logFromMid2024 < cutoffJan1, + true, + "Log from mid-2024 should be deleted on Jan 1, 2026" + ); + } + + console.log("All calculateCutoffTimestamp tests passed!"); +} + +// Run all tests +try { + testCalculateCutoffTimestamp(); + console.log("All tests passed successfully!"); +} catch (error) { + console.error("Test failed:", error); + process.exit(1); +} diff --git a/server/lib/cleanupLogs.ts b/server/lib/cleanupLogs.ts index 847e5d5d..7cdd6e3e 100644 --- a/server/lib/cleanupLogs.ts +++ b/server/lib/cleanupLogs.ts @@ -37,14 +37,14 @@ export function initLogCleanupInterval() { if (settingsLogRetentionDaysAction > 0) { await cleanUpOldActionLogs( orgId, - settingsLogRetentionDaysRequest + settingsLogRetentionDaysAction ); } if (settingsLogRetentionDaysAccess > 0) { await cleanUpOldAccessLogs( orgId, - settingsLogRetentionDaysRequest + settingsLogRetentionDaysAccess ); } @@ -56,7 +56,21 @@ export function initLogCleanupInterval() { } } }, - // 3 * 60 * 60 * 1000 - 60 * 1000 // for testing + 3 * 60 * 60 * 1000 ); // every 3 hours } + +export function calculateCutoffTimestamp(retentionDays: number): number { + const now = Math.floor(Date.now() / 1000); + if (retentionDays === 9001) { + // Special case: data is erased at the end of the year following the year it was generated + // This means we delete logs from 2 years ago or older (logs from year Y are deleted after Dec 31 of year Y+1) + const currentYear = new Date().getFullYear(); + // Cutoff is the start of the year before last (Jan 1, currentYear - 1 at 00:00:00) + // Any logs before this date are from 2+ years ago and should be deleted + const cutoffDate = new Date(Date.UTC(currentYear - 1, 0, 1, 0, 0, 0)); + return Math.floor(cutoffDate.getTime() / 1000); + } else { + return now - retentionDays * 24 * 60 * 60; + } +} diff --git a/server/lib/consts.ts b/server/lib/consts.ts index b014f849..b380023e 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.12.3"; +export const APP_VERSION = "1.13.0-rc.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/domainUtils.ts b/server/lib/domainUtils.ts index d043ca51..3562df68 100644 --- a/server/lib/domainUtils.ts +++ b/server/lib/domainUtils.ts @@ -4,18 +4,20 @@ import { eq, and } from "drizzle-orm"; import { subdomainSchema } from "@server/lib/schemas"; import { fromError } from "zod-validation-error"; -export type DomainValidationResult = { - success: true; - fullDomain: string; - subdomain: string | null; -} | { - success: false; - error: string; -}; +export type DomainValidationResult = + | { + success: true; + fullDomain: string; + subdomain: string | null; + } + | { + success: false; + error: string; + }; /** * Validates a domain and constructs the full domain based on domain type and subdomain. - * + * * @param domainId - The ID of the domain to validate * @param orgId - The organization ID to check domain access * @param subdomain - Optional subdomain to append (for ns and wildcard domains) @@ -34,7 +36,10 @@ export async function validateAndConstructDomain( .where(eq(domains.domainId, domainId)) .leftJoin( orgDomains, - and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) + and( + eq(orgDomains.orgId, orgId), + eq(orgDomains.domainId, domainId) + ) ); // Check if domain exists @@ -106,7 +111,7 @@ export async function validateAndConstructDomain( } catch (error) { return { success: false, - error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}` + error: `An error occurred while validating domain: ${error instanceof Error ? error.message : "Unknown error"}` }; } } diff --git a/server/lib/encryption.ts b/server/lib/encryption.ts index 7959fa4b..79caecd1 100644 --- a/server/lib/encryption.ts +++ b/server/lib/encryption.ts @@ -1,39 +1,39 @@ -import crypto from 'crypto'; +import crypto from "crypto"; export function encryptData(data: string, key: Buffer): string { - const algorithm = 'aes-256-gcm'; - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv(algorithm, key, iv); - - let encrypted = cipher.update(data, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - - const authTag = cipher.getAuthTag(); - - // Combine IV, auth tag, and encrypted data - return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted; + const algorithm = "aes-256-gcm"; + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, key, iv); + + let encrypted = cipher.update(data, "utf8", "hex"); + encrypted += cipher.final("hex"); + + const authTag = cipher.getAuthTag(); + + // Combine IV, auth tag, and encrypted data + return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted; } // Helper function to decrypt data (you'll need this to read certificates) export function decryptData(encryptedData: string, key: Buffer): string { - const algorithm = 'aes-256-gcm'; - const parts = encryptedData.split(':'); - - if (parts.length !== 3) { - throw new Error('Invalid encrypted data format'); - } - - const iv = Buffer.from(parts[0], 'hex'); - const authTag = Buffer.from(parts[1], 'hex'); - const encrypted = parts[2]; - - const decipher = crypto.createDecipheriv(algorithm, key, iv); - decipher.setAuthTag(authTag); - - let decrypted = decipher.update(encrypted, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - - return decrypted; + const algorithm = "aes-256-gcm"; + const parts = encryptedData.split(":"); + + if (parts.length !== 3) { + throw new Error("Invalid encrypted data format"); + } + + const iv = Buffer.from(parts[0], "hex"); + const authTag = Buffer.from(parts[1], "hex"); + const encrypted = parts[2]; + + const decipher = crypto.createDecipheriv(algorithm, key, iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, "hex", "utf8"); + decrypted += decipher.final("utf8"); + + return decrypted; } -// openssl rand -hex 32 > config/encryption.key \ No newline at end of file +// openssl rand -hex 32 > config/encryption.key diff --git a/server/lib/exitNodes/getCurrentExitNodeId.ts b/server/lib/exitNodes/getCurrentExitNodeId.ts index d895ce42..1e5c10e3 100644 --- a/server/lib/exitNodes/getCurrentExitNodeId.ts +++ b/server/lib/exitNodes/getCurrentExitNodeId.ts @@ -30,4 +30,4 @@ export async function getCurrentExitNodeId(): Promise { } } return currentExitNodeId; -} \ No newline at end of file +} diff --git a/server/lib/exitNodes/index.ts b/server/lib/exitNodes/index.ts index ba30ccc2..d1477a68 100644 --- a/server/lib/exitNodes/index.ts +++ b/server/lib/exitNodes/index.ts @@ -1,4 +1,4 @@ export * from "./exitNodes"; export * from "./exitNodeComms"; export * from "./subnet"; -export * from "./getCurrentExitNodeId"; \ No newline at end of file +export * from "./getCurrentExitNodeId"; diff --git a/server/lib/exitNodes/subnet.ts b/server/lib/exitNodes/subnet.ts index c06f1d05..49e28bd5 100644 --- a/server/lib/exitNodes/subnet.ts +++ b/server/lib/exitNodes/subnet.ts @@ -27,4 +27,4 @@ export async function getNextAvailableSubnet(): Promise { "/" + subnet.split("/")[1]; return subnet; -} \ No newline at end of file +} diff --git a/server/lib/geoip.ts b/server/lib/geoip.ts index 5bc29ef9..8eea4d6f 100644 --- a/server/lib/geoip.ts +++ b/server/lib/geoip.ts @@ -30,4 +30,4 @@ export async function getCountryCodeForIp( } return; -} \ No newline at end of file +} diff --git a/server/lib/idp/generateRedirectUrl.ts b/server/lib/idp/generateRedirectUrl.ts index 077ac6f6..cf55e161 100644 --- a/server/lib/idp/generateRedirectUrl.ts +++ b/server/lib/idp/generateRedirectUrl.ts @@ -33,7 +33,11 @@ export async function generateOidcRedirectUrl( ) .limit(1); - if (res?.loginPage && res.loginPage.domainId && res.loginPage.fullDomain) { + if ( + res?.loginPage && + res.loginPage.domainId && + res.loginPage.fullDomain + ) { baseUrl = `${method}://${res.loginPage.fullDomain}`; } } diff --git a/server/lib/ip.test.ts b/server/lib/ip.test.ts index 67a2faaa..70436e05 100644 --- a/server/lib/ip.test.ts +++ b/server/lib/ip.test.ts @@ -4,7 +4,7 @@ import { assertEquals } from "@test/assert"; // Test cases function testFindNextAvailableCidr() { console.log("Running findNextAvailableCidr tests..."); - + // Test 0: Basic IPv4 allocation with a subnet in the wrong range { const existing = ["100.90.130.1/30", "100.90.128.4/30"]; @@ -23,7 +23,11 @@ function testFindNextAvailableCidr() { { const existing = ["10.0.0.0/16", "10.2.0.0/16"]; const result = findNextAvailableCidr(existing, 16, "10.0.0.0/8"); - assertEquals(result, "10.1.0.0/16", "Finding gap between allocations failed"); + assertEquals( + result, + "10.1.0.0/16", + "Finding gap between allocations failed" + ); } // Test 3: No available space @@ -33,7 +37,7 @@ function testFindNextAvailableCidr() { assertEquals(result, null, "No available space test failed"); } - // Test 4: Empty existing + // Test 4: Empty existing { const existing: string[] = []; const result = findNextAvailableCidr(existing, 30, "10.0.0.0/8"); @@ -137,4 +141,4 @@ try { } catch (error) { console.error("Test failed:", error); process.exit(1); -} \ No newline at end of file +} diff --git a/server/lib/ip.ts b/server/lib/ip.ts index f9b3cb61..36065df3 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -116,6 +116,68 @@ function bigIntToIp(num: bigint, version: IPVersion): string { } } +/** + * Parses an endpoint string (ip:port) handling both IPv4 and IPv6 addresses. + * IPv6 addresses may be bracketed like [::1]:8080 or unbracketed like ::1:8080. + * For unbracketed IPv6, the last colon-separated segment is treated as the port. + * + * @param endpoint The endpoint string to parse (e.g., "192.168.1.1:8080" or "[::1]:8080" or "2607:fea8::1:8080") + * @returns An object with ip and port, or null if parsing fails + */ +export function parseEndpoint(endpoint: string): { ip: string; port: number } | null { + if (!endpoint) return null; + + // Check for bracketed IPv6 format: [ip]:port + const bracketedMatch = endpoint.match(/^\[([^\]]+)\]:(\d+)$/); + if (bracketedMatch) { + const ip = bracketedMatch[1]; + const port = parseInt(bracketedMatch[2], 10); + if (isNaN(port)) return null; + return { ip, port }; + } + + // Check if this looks like IPv6 (contains multiple colons) + const colonCount = (endpoint.match(/:/g) || []).length; + + if (colonCount > 1) { + // This is IPv6 - the port is after the last colon + const lastColonIndex = endpoint.lastIndexOf(":"); + const ip = endpoint.substring(0, lastColonIndex); + const portStr = endpoint.substring(lastColonIndex + 1); + const port = parseInt(portStr, 10); + if (isNaN(port)) return null; + return { ip, port }; + } + + // IPv4 format: ip:port + if (colonCount === 1) { + const [ip, portStr] = endpoint.split(":"); + const port = parseInt(portStr, 10); + if (isNaN(port)) return null; + return { ip, port }; + } + + return null; +} + +/** + * Formats an IP and port into a consistent endpoint string. + * IPv6 addresses are wrapped in brackets for proper parsing. + * + * @param ip The IP address (IPv4 or IPv6) + * @param port The port number + * @returns Formatted endpoint string + */ +export function formatEndpoint(ip: string, port: number): string { + // Check if this is IPv6 (contains colons) + if (ip.includes(":")) { + // Remove brackets if already present + const cleanIp = ip.replace(/^\[|\]$/g, ""); + return `[${cleanIp}]:${port}`; + } + return `${ip}:${port}`; +} + /** * Converts CIDR to IP range */ @@ -244,9 +306,13 @@ export function isIpInCidr(ip: string, cidr: string): boolean { } export async function getNextAvailableClientSubnet( - orgId: string + orgId: string, + transaction: Transaction | typeof db = db ): Promise { - const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId)); + const [org] = await transaction + .select() + .from(orgs) + .where(eq(orgs.orgId, orgId)); if (!org) { throw new Error(`Organization with ID ${orgId} not found`); @@ -256,14 +322,14 @@ export async function getNextAvailableClientSubnet( throw new Error(`Organization with ID ${orgId} has no subnet defined`); } - const existingAddressesSites = await db + const existingAddressesSites = await transaction .select({ address: sites.address }) .from(sites) .where(and(isNotNull(sites.address), eq(sites.orgId, orgId))); - const existingAddressesClients = await db + const existingAddressesClients = await transaction .select({ address: clients.subnet }) @@ -359,7 +425,9 @@ export async function getNextAvailableOrgSubnet(): Promise { return subnet; } -export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[] { +export function generateRemoteSubnets( + allSiteResources: SiteResource[] +): string[] { const remoteSubnets = allSiteResources .filter((sr) => { if (sr.mode === "cidr") return true; diff --git a/server/lib/logAccessAudit.ts b/server/lib/logAccessAudit.ts index 82ddda67..5f3601da 100644 --- a/server/lib/logAccessAudit.ts +++ b/server/lib/logAccessAudit.ts @@ -14,4 +14,4 @@ export async function logAccessAudit(data: { requestIp?: string; }) { return; -} \ No newline at end of file +} diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index ac819619..fe610663 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -14,7 +14,8 @@ export const configSchema = z .object({ app: z .object({ - dashboard_url: z.url() + dashboard_url: z + .url() .pipe(z.url()) .transform((url) => url.toLowerCase()) .optional(), @@ -255,7 +256,10 @@ export const configSchema = z .object({ block_size: z.number().positive().gt(0).optional().default(24), subnet_group: z.string().optional().default("100.90.128.0/24"), - utility_subnet_group: z.string().optional().default("100.96.128.0/24") //just hardcode this for now as well + utility_subnet_group: z + .string() + .optional() + .default("100.96.128.0/24") //just hardcode this for now as well }) .optional() .default({ diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index 45a992cc..549dbffe 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -24,7 +24,7 @@ import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers"; import { - initPeerAddHandshake as holepunchSiteAdd, + initPeerAddHandshake, deletePeer as olmDeletePeer } from "@server/routers/olm/peers"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; @@ -33,6 +33,8 @@ import { generateAliasConfig, generateRemoteSubnets, generateSubnetProxyTargets, + parseEndpoint, + formatEndpoint } from "@server/lib/ip"; import { addPeerData, @@ -109,21 +111,22 @@ export async function getClientSiteResourceAccess( const directClientIds = allClientSiteResources.map((row) => row.clientId); // Get full client details for directly associated clients - const directClients = directClientIds.length > 0 - ? await trx - .select({ - clientId: clients.clientId, - pubKey: clients.pubKey, - subnet: clients.subnet - }) - .from(clients) - .where( - and( - inArray(clients.clientId, directClientIds), - eq(clients.orgId, siteResource.orgId) // filter by org to prevent cross-org associations + const directClients = + directClientIds.length > 0 + ? await trx + .select({ + clientId: clients.clientId, + pubKey: clients.pubKey, + subnet: clients.subnet + }) + .from(clients) + .where( + and( + inArray(clients.clientId, directClientIds), + eq(clients.orgId, siteResource.orgId) // filter by org to prevent cross-org associations + ) ) - ) - : []; + : []; // Merge user-based clients with directly associated clients const allClientsMap = new Map( @@ -474,7 +477,7 @@ async function handleMessagesForSiteClients( } if (isAdd) { - await holepunchSiteAdd( + await initPeerAddHandshake( // this will kick off the add peer process for the client client.clientId, { @@ -537,7 +540,18 @@ export async function updateClientSiteDestinations( } if (!site.clientSitesAssociationsCache.endpoint) { - logger.warn(`Site ${site.sites.siteId} has no endpoint, skipping`); // if this is a new association the endpoint is not set yet // TODO: FIX THIS + // if this is a new association the endpoint is not set yet + continue; + } + + // Parse the endpoint properly for both IPv4 and IPv6 + const parsedEndpoint = parseEndpoint( + site.clientSitesAssociationsCache.endpoint + ); + if (!parsedEndpoint) { + logger.warn( + `Failed to parse endpoint ${site.clientSitesAssociationsCache.endpoint}, skipping` + ); continue; } @@ -552,13 +566,8 @@ export async function updateClientSiteDestinations( exitNodeId: site.exitNodes?.exitNodeId || 0, type: site.exitNodes?.type || "", name: site.exitNodes?.name || "", - sourceIp: - site.clientSitesAssociationsCache.endpoint.split(":")[0] || - "", - sourcePort: - parseInt( - site.clientSitesAssociationsCache.endpoint.split(":")[1] - ) || 0, + sourceIp: parsedEndpoint.ip, + sourcePort: parsedEndpoint.port, destinations: [ { destinationIP: site.sites.subnet.split("/")[0], @@ -701,11 +710,46 @@ async function handleSubnetProxyTargetUpdates( } for (const client of removedClients) { + // Check if this client still has access to another resource on this site with the same destination + const destinationStillInUse = await trx + .select() + .from(siteResources) + .innerJoin( + clientSiteResourcesAssociationsCache, + eq( + clientSiteResourcesAssociationsCache.siteResourceId, + siteResources.siteResourceId + ) + ) + .where( + and( + eq( + clientSiteResourcesAssociationsCache.clientId, + client.clientId + ), + eq(siteResources.siteId, siteResource.siteId), + eq( + siteResources.destination, + siteResource.destination + ), + ne( + siteResources.siteResourceId, + siteResource.siteResourceId + ) + ) + ); + + // Only remove remote subnet if no other resource uses the same destination + const remoteSubnetsToRemove = + destinationStillInUse.length > 0 + ? [] + : generateRemoteSubnets([siteResource]); + olmJobs.push( removePeerData( client.clientId, siteResource.siteId, - generateRemoteSubnets([siteResource]), + remoteSubnetsToRemove, generateAliasConfig([siteResource]) ) ); @@ -783,7 +827,10 @@ export async function rebuildClientAssociationsFromClient( .from(roleSiteResources) .innerJoin( siteResources, - eq(siteResources.siteResourceId, roleSiteResources.siteResourceId) + eq( + siteResources.siteResourceId, + roleSiteResources.siteResourceId + ) ) .where( and( @@ -1213,12 +1260,47 @@ async function handleMessagesForClientResources( } try { + // Check if this client still has access to another resource on this site with the same destination + const destinationStillInUse = await trx + .select() + .from(siteResources) + .innerJoin( + clientSiteResourcesAssociationsCache, + eq( + clientSiteResourcesAssociationsCache.siteResourceId, + siteResources.siteResourceId + ) + ) + .where( + and( + eq( + clientSiteResourcesAssociationsCache.clientId, + client.clientId + ), + eq(siteResources.siteId, resource.siteId), + eq( + siteResources.destination, + resource.destination + ), + ne( + siteResources.siteResourceId, + resource.siteResourceId + ) + ) + ); + + // Only remove remote subnet if no other resource uses the same destination + const remoteSubnetsToRemove = + destinationStillInUse.length > 0 + ? [] + : generateRemoteSubnets([resource]); + // Remove peer data from olm olmJobs.push( removePeerData( client.clientId, resource.siteId, - generateRemoteSubnets([resource]), + remoteSubnetsToRemove, generateAliasConfig([resource]) ) ); diff --git a/server/lib/resend.ts b/server/lib/resend.ts index 0af039bb..0c21b1be 100644 --- a/server/lib/resend.ts +++ b/server/lib/resend.ts @@ -1,8 +1,8 @@ export enum AudienceIds { - SignUps = "", - Subscribed = "", - Churned = "", - Newsletter = "" + SignUps = "", + Subscribed = "", + Churned = "", + Newsletter = "" } let resend; diff --git a/server/lib/response.ts b/server/lib/response.ts index ae8461ba..fd8fa89f 100644 --- a/server/lib/response.ts +++ b/server/lib/response.ts @@ -3,14 +3,14 @@ import { Response } from "express"; export const response = ( res: Response, - { data, success, error, message, status }: ResponseT, + { data, success, error, message, status }: ResponseT ) => { return res.status(status).send({ data, success, error, message, - status, + status }); }; diff --git a/server/lib/s3.ts b/server/lib/s3.ts index 5fc3318f..17314ed7 100644 --- a/server/lib/s3.ts +++ b/server/lib/s3.ts @@ -1,5 +1,5 @@ import { S3Client } from "@aws-sdk/client-s3"; export const s3Client = new S3Client({ - region: process.env.S3_REGION || "us-east-1", + region: process.env.S3_REGION || "us-east-1" }); diff --git a/server/lib/serverIpService.ts b/server/lib/serverIpService.ts index 8c16fd43..7f423f9b 100644 --- a/server/lib/serverIpService.ts +++ b/server/lib/serverIpService.ts @@ -6,7 +6,7 @@ let serverIp: string | null = null; const services = [ "https://checkip.amazonaws.com", "https://ifconfig.io/ip", - "https://api.ipify.org", + "https://api.ipify.org" ]; export async function fetchServerIp() { @@ -17,7 +17,9 @@ export async function fetchServerIp() { logger.debug("Detected public IP: " + serverIp); return; } catch (err: any) { - console.warn(`Failed to fetch server IP from ${url}: ${err.message || err.code}`); + console.warn( + `Failed to fetch server IP from ${url}: ${err.message || err.code}` + ); } } diff --git a/server/lib/stoi.ts b/server/lib/stoi.ts index ebc789e6..3c869858 100644 --- a/server/lib/stoi.ts +++ b/server/lib/stoi.ts @@ -1,8 +1,7 @@ export default function stoi(val: any) { if (typeof val === "string") { - return parseInt(val); + return parseInt(val); + } else { + return val; } - else { - return val; - } -} \ No newline at end of file +} diff --git a/server/lib/traefik/TraefikConfigManager.ts b/server/lib/traefik/TraefikConfigManager.ts index 56648559..46d5ccc8 100644 --- a/server/lib/traefik/TraefikConfigManager.ts +++ b/server/lib/traefik/TraefikConfigManager.ts @@ -142,8 +142,24 @@ export class TraefikConfigManager { const wildcardExists = await this.fileExists(wildcardPath); let lastModified: Date | null = null; - const expiresAt: Date | null = null; + let expiresAt: number | null = null; let wildcard = false; + const expiresAtPath = path.join(domainDir, ".expires_at"); + const expiresAtExists = await this.fileExists(expiresAtPath); + + if (expiresAtExists) { + try { + const expiresAtStr = fs + .readFileSync(expiresAtPath, "utf8") + .trim(); + expiresAt = parseInt(expiresAtStr, 10); + if (isNaN(expiresAt)) { + expiresAt = null; + } + } catch { + expiresAt = null; + } + } if (lastUpdateExists) { try { @@ -179,7 +195,9 @@ export class TraefikConfigManager { state.set(domain, { exists: certExists && keyExists, - lastModified, + lastModified: lastModified + ? Math.floor(lastModified.getTime() / 1000) + : null, expiresAt, wildcard }); @@ -259,9 +277,9 @@ export class TraefikConfigManager { // Check if certificate is expiring soon (within 30 days) if (localState.expiresAt) { - const daysUntilExpiry = - (localState.expiresAt - Math.floor(Date.now() / 1000)) / - (1000 * 60 * 60 * 24); + const nowInSeconds = Math.floor(Date.now() / 1000); + const secondsUntilExpiry = localState.expiresAt - nowInSeconds; + const daysUntilExpiry = secondsUntilExpiry / (60 * 60 * 24); if (daysUntilExpiry < 30) { logger.info( `Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)` @@ -448,7 +466,9 @@ export class TraefikConfigManager { config.getRawConfig().traefik.site_types, build == "oss", // filter out the namespace domains in open source build != "oss", // generate the login pages on the cloud and hybrid, - build == "saas" ? false : config.getRawConfig().traefik.allow_raw_resources // dont allow raw resources on saas otherwise use config + build == "saas" + ? false + : config.getRawConfig().traefik.allow_raw_resources // dont allow raw resources on saas otherwise use config ); const domains = new Set(); @@ -773,16 +793,27 @@ export class TraefikConfigManager { logger.info( `Certificate updated for domain: ${cert.domain}${cert.wildcard ? " (wildcard)" : ""}` ); - - // Update local state tracking - this.lastLocalCertificateState.set(cert.domain, { - exists: true, - lastModified: Math.floor(Date.now() / 1000), - expiresAt: cert.expiresAt, - wildcard: cert.wildcard - }); } + // Always update expiry tracking when we fetch a certificate, + // even if the cert content didn't change + if (cert.expiresAt) { + const expiresAtPath = path.join(domainDir, ".expires_at"); + fs.writeFileSync( + expiresAtPath, + cert.expiresAt.toString(), + "utf8" + ); + } + + // Update local state tracking + this.lastLocalCertificateState.set(cert.domain, { + exists: true, + lastModified: Math.floor(Date.now() / 1000), + expiresAt: cert.expiresAt, + wildcard: cert.wildcard + }); + // Always ensure the config entry exists and is up to date const certEntry = { certFile: certPath, diff --git a/server/lib/traefik/index.ts b/server/lib/traefik/index.ts index 5630028c..0fc483fa 100644 --- a/server/lib/traefik/index.ts +++ b/server/lib/traefik/index.ts @@ -1 +1 @@ -export * from "./getTraefikConfig"; \ No newline at end of file +export * from "./getTraefikConfig"; diff --git a/server/lib/traefik/traefikConfig.test.ts b/server/lib/traefik/traefikConfig.test.ts index 88e5da49..36ad4e68 100644 --- a/server/lib/traefik/traefikConfig.test.ts +++ b/server/lib/traefik/traefikConfig.test.ts @@ -2,234 +2,249 @@ import { assertEquals } from "@test/assert"; import { isDomainCoveredByWildcard } from "./TraefikConfigManager"; function runTests() { - console.log('Running wildcard domain coverage tests...'); - + console.log("Running wildcard domain coverage tests..."); + // Test case 1: Basic wildcard certificate at example.com const basicWildcardCerts = new Map([ - ['example.com', { exists: true, wildcard: true }] + ["example.com", { exists: true, wildcard: true }] ]); - + // Should match first-level subdomains assertEquals( - isDomainCoveredByWildcard('level1.example.com', basicWildcardCerts), + isDomainCoveredByWildcard("level1.example.com", basicWildcardCerts), true, - 'Wildcard cert at example.com should match level1.example.com' + "Wildcard cert at example.com should match level1.example.com" ); - + assertEquals( - isDomainCoveredByWildcard('api.example.com', basicWildcardCerts), + isDomainCoveredByWildcard("api.example.com", basicWildcardCerts), true, - 'Wildcard cert at example.com should match api.example.com' + "Wildcard cert at example.com should match api.example.com" ); - + assertEquals( - isDomainCoveredByWildcard('www.example.com', basicWildcardCerts), + isDomainCoveredByWildcard("www.example.com", basicWildcardCerts), true, - 'Wildcard cert at example.com should match www.example.com' + "Wildcard cert at example.com should match www.example.com" ); - + // Should match the root domain (exact match) assertEquals( - isDomainCoveredByWildcard('example.com', basicWildcardCerts), + isDomainCoveredByWildcard("example.com", basicWildcardCerts), true, - 'Wildcard cert at example.com should match example.com itself' + "Wildcard cert at example.com should match example.com itself" ); - + // Should NOT match second-level subdomains assertEquals( - isDomainCoveredByWildcard('level2.level1.example.com', basicWildcardCerts), + isDomainCoveredByWildcard( + "level2.level1.example.com", + basicWildcardCerts + ), false, - 'Wildcard cert at example.com should NOT match level2.level1.example.com' + "Wildcard cert at example.com should NOT match level2.level1.example.com" ); - + assertEquals( - isDomainCoveredByWildcard('deep.nested.subdomain.example.com', basicWildcardCerts), + isDomainCoveredByWildcard( + "deep.nested.subdomain.example.com", + basicWildcardCerts + ), false, - 'Wildcard cert at example.com should NOT match deep.nested.subdomain.example.com' + "Wildcard cert at example.com should NOT match deep.nested.subdomain.example.com" ); - + // Should NOT match different domains assertEquals( - isDomainCoveredByWildcard('test.otherdomain.com', basicWildcardCerts), + isDomainCoveredByWildcard("test.otherdomain.com", basicWildcardCerts), false, - 'Wildcard cert at example.com should NOT match test.otherdomain.com' + "Wildcard cert at example.com should NOT match test.otherdomain.com" ); - + assertEquals( - isDomainCoveredByWildcard('notexample.com', basicWildcardCerts), + isDomainCoveredByWildcard("notexample.com", basicWildcardCerts), false, - 'Wildcard cert at example.com should NOT match notexample.com' + "Wildcard cert at example.com should NOT match notexample.com" ); - + // Test case 2: Multiple wildcard certificates const multipleWildcardCerts = new Map([ - ['example.com', { exists: true, wildcard: true }], - ['test.org', { exists: true, wildcard: true }], - ['api.service.net', { exists: true, wildcard: true }] + ["example.com", { exists: true, wildcard: true }], + ["test.org", { exists: true, wildcard: true }], + ["api.service.net", { exists: true, wildcard: true }] ]); - + assertEquals( - isDomainCoveredByWildcard('app.example.com', multipleWildcardCerts), + isDomainCoveredByWildcard("app.example.com", multipleWildcardCerts), true, - 'Should match subdomain of first wildcard cert' + "Should match subdomain of first wildcard cert" ); - + assertEquals( - isDomainCoveredByWildcard('staging.test.org', multipleWildcardCerts), + isDomainCoveredByWildcard("staging.test.org", multipleWildcardCerts), true, - 'Should match subdomain of second wildcard cert' + "Should match subdomain of second wildcard cert" ); - + assertEquals( - isDomainCoveredByWildcard('v1.api.service.net', multipleWildcardCerts), + isDomainCoveredByWildcard("v1.api.service.net", multipleWildcardCerts), true, - 'Should match subdomain of third wildcard cert' + "Should match subdomain of third wildcard cert" ); - + assertEquals( - isDomainCoveredByWildcard('deep.nested.api.service.net', multipleWildcardCerts), + isDomainCoveredByWildcard( + "deep.nested.api.service.net", + multipleWildcardCerts + ), false, - 'Should NOT match multi-level subdomain of third wildcard cert' + "Should NOT match multi-level subdomain of third wildcard cert" ); - + // Test exact domain matches for multiple certs assertEquals( - isDomainCoveredByWildcard('example.com', multipleWildcardCerts), + isDomainCoveredByWildcard("example.com", multipleWildcardCerts), true, - 'Should match exact domain of first wildcard cert' + "Should match exact domain of first wildcard cert" ); - + assertEquals( - isDomainCoveredByWildcard('test.org', multipleWildcardCerts), + isDomainCoveredByWildcard("test.org", multipleWildcardCerts), true, - 'Should match exact domain of second wildcard cert' + "Should match exact domain of second wildcard cert" ); - + assertEquals( - isDomainCoveredByWildcard('api.service.net', multipleWildcardCerts), + isDomainCoveredByWildcard("api.service.net", multipleWildcardCerts), true, - 'Should match exact domain of third wildcard cert' + "Should match exact domain of third wildcard cert" ); - + // Test case 3: Non-wildcard certificates (should not match anything) const nonWildcardCerts = new Map([ - ['example.com', { exists: true, wildcard: false }], - ['specific.domain.com', { exists: true, wildcard: false }] + ["example.com", { exists: true, wildcard: false }], + ["specific.domain.com", { exists: true, wildcard: false }] ]); - + assertEquals( - isDomainCoveredByWildcard('sub.example.com', nonWildcardCerts), + isDomainCoveredByWildcard("sub.example.com", nonWildcardCerts), false, - 'Non-wildcard cert should not match subdomains' + "Non-wildcard cert should not match subdomains" ); - + assertEquals( - isDomainCoveredByWildcard('example.com', nonWildcardCerts), + isDomainCoveredByWildcard("example.com", nonWildcardCerts), false, - 'Non-wildcard cert should not match even exact domain via this function' + "Non-wildcard cert should not match even exact domain via this function" ); - + // Test case 4: Non-existent certificates (should not match) const nonExistentCerts = new Map([ - ['example.com', { exists: false, wildcard: true }], - ['missing.com', { exists: false, wildcard: true }] + ["example.com", { exists: false, wildcard: true }], + ["missing.com", { exists: false, wildcard: true }] ]); - + assertEquals( - isDomainCoveredByWildcard('sub.example.com', nonExistentCerts), + isDomainCoveredByWildcard("sub.example.com", nonExistentCerts), false, - 'Non-existent wildcard cert should not match' + "Non-existent wildcard cert should not match" ); - + // Test case 5: Edge cases with special domain names const specialDomainCerts = new Map([ - ['localhost', { exists: true, wildcard: true }], - ['127-0-0-1.nip.io', { exists: true, wildcard: true }], - ['xn--e1afmkfd.xn--p1ai', { exists: true, wildcard: true }] // IDN domain + ["localhost", { exists: true, wildcard: true }], + ["127-0-0-1.nip.io", { exists: true, wildcard: true }], + ["xn--e1afmkfd.xn--p1ai", { exists: true, wildcard: true }] // IDN domain ]); - + assertEquals( - isDomainCoveredByWildcard('app.localhost', specialDomainCerts), + isDomainCoveredByWildcard("app.localhost", specialDomainCerts), true, - 'Should match subdomain of localhost wildcard' + "Should match subdomain of localhost wildcard" ); - + assertEquals( - isDomainCoveredByWildcard('test.127-0-0-1.nip.io', specialDomainCerts), + isDomainCoveredByWildcard("test.127-0-0-1.nip.io", specialDomainCerts), true, - 'Should match subdomain of nip.io wildcard' + "Should match subdomain of nip.io wildcard" ); - + assertEquals( - isDomainCoveredByWildcard('sub.xn--e1afmkfd.xn--p1ai', specialDomainCerts), + isDomainCoveredByWildcard( + "sub.xn--e1afmkfd.xn--p1ai", + specialDomainCerts + ), true, - 'Should match subdomain of IDN wildcard' + "Should match subdomain of IDN wildcard" ); - + // Test case 6: Empty input and edge cases const emptyCerts = new Map(); - + assertEquals( - isDomainCoveredByWildcard('any.domain.com', emptyCerts), + isDomainCoveredByWildcard("any.domain.com", emptyCerts), false, - 'Empty certificate map should not match any domain' + "Empty certificate map should not match any domain" ); - + // Test case 7: Domains with single character components const singleCharCerts = new Map([ - ['a.com', { exists: true, wildcard: true }], - ['x.y.z', { exists: true, wildcard: true }] + ["a.com", { exists: true, wildcard: true }], + ["x.y.z", { exists: true, wildcard: true }] ]); - + assertEquals( - isDomainCoveredByWildcard('b.a.com', singleCharCerts), + isDomainCoveredByWildcard("b.a.com", singleCharCerts), true, - 'Should match single character subdomain' + "Should match single character subdomain" ); - + assertEquals( - isDomainCoveredByWildcard('w.x.y.z', singleCharCerts), + isDomainCoveredByWildcard("w.x.y.z", singleCharCerts), true, - 'Should match single character subdomain of multi-part domain' + "Should match single character subdomain of multi-part domain" ); - + assertEquals( - isDomainCoveredByWildcard('v.w.x.y.z', singleCharCerts), + isDomainCoveredByWildcard("v.w.x.y.z", singleCharCerts), false, - 'Should NOT match multi-level subdomain of single char domain' + "Should NOT match multi-level subdomain of single char domain" ); - + // Test case 8: Domains with numbers and hyphens const numericCerts = new Map([ - ['api-v2.service-1.com', { exists: true, wildcard: true }], - ['123.456.net', { exists: true, wildcard: true }] + ["api-v2.service-1.com", { exists: true, wildcard: true }], + ["123.456.net", { exists: true, wildcard: true }] ]); - + assertEquals( - isDomainCoveredByWildcard('staging.api-v2.service-1.com', numericCerts), + isDomainCoveredByWildcard("staging.api-v2.service-1.com", numericCerts), true, - 'Should match subdomain with hyphens and numbers' + "Should match subdomain with hyphens and numbers" ); - + assertEquals( - isDomainCoveredByWildcard('test.123.456.net', numericCerts), + isDomainCoveredByWildcard("test.123.456.net", numericCerts), true, - 'Should match subdomain with numeric components' + "Should match subdomain with numeric components" ); - + assertEquals( - isDomainCoveredByWildcard('deep.staging.api-v2.service-1.com', numericCerts), + isDomainCoveredByWildcard( + "deep.staging.api-v2.service-1.com", + numericCerts + ), false, - 'Should NOT match multi-level subdomain with hyphens and numbers' + "Should NOT match multi-level subdomain with hyphens and numbers" ); - - console.log('All wildcard domain coverage tests passed!'); + + console.log("All wildcard domain coverage tests passed!"); } // Run all tests try { runTests(); } catch (error) { - console.error('Test failed:', error); + console.error("Test failed:", error); process.exit(1); } diff --git a/server/lib/traefik/utils.ts b/server/lib/traefik/utils.ts index 37ebfa0b..ec0eae5b 100644 --- a/server/lib/traefik/utils.ts +++ b/server/lib/traefik/utils.ts @@ -31,12 +31,17 @@ export function validatePathRewriteConfig( } if (rewritePathType !== "stripPrefix") { - if ((rewritePath && !rewritePathType) || (!rewritePath && rewritePathType)) { - return { isValid: false, error: "Both rewritePath and rewritePathType must be specified together" }; + if ( + (rewritePath && !rewritePathType) || + (!rewritePath && rewritePathType) + ) { + return { + isValid: false, + error: "Both rewritePath and rewritePathType must be specified together" + }; } } - if (!rewritePath || !rewritePathType) { return { isValid: true }; } @@ -68,14 +73,14 @@ export function validatePathRewriteConfig( } } - // Additional validation for stripPrefix if (rewritePathType === "stripPrefix") { if (pathMatchType !== "prefix") { - logger.warn(`stripPrefix rewrite type is most effective with prefix path matching. Current match type: ${pathMatchType}`); + logger.warn( + `stripPrefix rewrite type is most effective with prefix path matching. Current match type: ${pathMatchType}` + ); } } return { isValid: true }; } - diff --git a/server/lib/validators.test.ts b/server/lib/validators.test.ts index e2043c74..c4c564cf 100644 --- a/server/lib/validators.test.ts +++ b/server/lib/validators.test.ts @@ -1,71 +1,247 @@ -import { isValidUrlGlobPattern } from "./validators"; +import { isValidUrlGlobPattern } from "./validators"; import { assertEquals } from "@test/assert"; function runTests() { - console.log('Running URL pattern validation tests...'); - + console.log("Running URL pattern validation tests..."); + // Test valid patterns - assertEquals(isValidUrlGlobPattern('simple'), true, 'Simple path segment should be valid'); - assertEquals(isValidUrlGlobPattern('simple/path'), true, 'Simple path with slash should be valid'); - assertEquals(isValidUrlGlobPattern('/leading/slash'), true, 'Path with leading slash should be valid'); - assertEquals(isValidUrlGlobPattern('path/'), true, 'Path with trailing slash should be valid'); - assertEquals(isValidUrlGlobPattern('path/*'), true, 'Path with wildcard segment should be valid'); - assertEquals(isValidUrlGlobPattern('*'), true, 'Single wildcard should be valid'); - assertEquals(isValidUrlGlobPattern('*/subpath'), true, 'Wildcard with subpath should be valid'); - assertEquals(isValidUrlGlobPattern('path/*/more'), true, 'Path with wildcard in the middle should be valid'); - + assertEquals( + isValidUrlGlobPattern("simple"), + true, + "Simple path segment should be valid" + ); + assertEquals( + isValidUrlGlobPattern("simple/path"), + true, + "Simple path with slash should be valid" + ); + assertEquals( + isValidUrlGlobPattern("/leading/slash"), + true, + "Path with leading slash should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path/"), + true, + "Path with trailing slash should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path/*"), + true, + "Path with wildcard segment should be valid" + ); + assertEquals( + isValidUrlGlobPattern("*"), + true, + "Single wildcard should be valid" + ); + assertEquals( + isValidUrlGlobPattern("*/subpath"), + true, + "Wildcard with subpath should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path/*/more"), + true, + "Path with wildcard in the middle should be valid" + ); + // Test with special characters - assertEquals(isValidUrlGlobPattern('path-with-dash'), true, 'Path with dash should be valid'); - assertEquals(isValidUrlGlobPattern('path_with_underscore'), true, 'Path with underscore should be valid'); - assertEquals(isValidUrlGlobPattern('path.with.dots'), true, 'Path with dots should be valid'); - assertEquals(isValidUrlGlobPattern('path~with~tilde'), true, 'Path with tilde should be valid'); - assertEquals(isValidUrlGlobPattern('path!with!exclamation'), true, 'Path with exclamation should be valid'); - assertEquals(isValidUrlGlobPattern('path$with$dollar'), true, 'Path with dollar should be valid'); - assertEquals(isValidUrlGlobPattern('path&with&ersand'), true, 'Path with ampersand should be valid'); - assertEquals(isValidUrlGlobPattern("path'with'quote"), true, "Path with quote should be valid"); - assertEquals(isValidUrlGlobPattern('path(with)parentheses'), true, 'Path with parentheses should be valid'); - assertEquals(isValidUrlGlobPattern('path+with+plus'), true, 'Path with plus should be valid'); - assertEquals(isValidUrlGlobPattern('path,with,comma'), true, 'Path with comma should be valid'); - assertEquals(isValidUrlGlobPattern('path;with;semicolon'), true, 'Path with semicolon should be valid'); - assertEquals(isValidUrlGlobPattern('path=with=equals'), true, 'Path with equals should be valid'); - assertEquals(isValidUrlGlobPattern('path:with:colon'), true, 'Path with colon should be valid'); - assertEquals(isValidUrlGlobPattern('path@with@at'), true, 'Path with at should be valid'); - + assertEquals( + isValidUrlGlobPattern("path-with-dash"), + true, + "Path with dash should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path_with_underscore"), + true, + "Path with underscore should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path.with.dots"), + true, + "Path with dots should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path~with~tilde"), + true, + "Path with tilde should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path!with!exclamation"), + true, + "Path with exclamation should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path$with$dollar"), + true, + "Path with dollar should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path&with&ersand"), + true, + "Path with ampersand should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path'with'quote"), + true, + "Path with quote should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path(with)parentheses"), + true, + "Path with parentheses should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path+with+plus"), + true, + "Path with plus should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path,with,comma"), + true, + "Path with comma should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path;with;semicolon"), + true, + "Path with semicolon should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path=with=equals"), + true, + "Path with equals should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path:with:colon"), + true, + "Path with colon should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path@with@at"), + true, + "Path with at should be valid" + ); + // Test with percent encoding - assertEquals(isValidUrlGlobPattern('path%20with%20spaces'), true, 'Path with percent-encoded spaces should be valid'); - assertEquals(isValidUrlGlobPattern('path%2Fwith%2Fencoded%2Fslashes'), true, 'Path with percent-encoded slashes should be valid'); - + assertEquals( + isValidUrlGlobPattern("path%20with%20spaces"), + true, + "Path with percent-encoded spaces should be valid" + ); + assertEquals( + isValidUrlGlobPattern("path%2Fwith%2Fencoded%2Fslashes"), + true, + "Path with percent-encoded slashes should be valid" + ); + // Test with wildcards in segments (the fixed functionality) - assertEquals(isValidUrlGlobPattern('padbootstrap*'), true, 'Path with wildcard at the end of segment should be valid'); - assertEquals(isValidUrlGlobPattern('pad*bootstrap'), true, 'Path with wildcard in the middle of segment should be valid'); - assertEquals(isValidUrlGlobPattern('*bootstrap'), true, 'Path with wildcard at the start of segment should be valid'); - assertEquals(isValidUrlGlobPattern('multiple*wildcards*in*segment'), true, 'Path with multiple wildcards in segment should be valid'); - assertEquals(isValidUrlGlobPattern('wild*/cards/in*/different/seg*ments'), true, 'Path with wildcards in different segments should be valid'); - + assertEquals( + isValidUrlGlobPattern("padbootstrap*"), + true, + "Path with wildcard at the end of segment should be valid" + ); + assertEquals( + isValidUrlGlobPattern("pad*bootstrap"), + true, + "Path with wildcard in the middle of segment should be valid" + ); + assertEquals( + isValidUrlGlobPattern("*bootstrap"), + true, + "Path with wildcard at the start of segment should be valid" + ); + assertEquals( + isValidUrlGlobPattern("multiple*wildcards*in*segment"), + true, + "Path with multiple wildcards in segment should be valid" + ); + assertEquals( + isValidUrlGlobPattern("wild*/cards/in*/different/seg*ments"), + true, + "Path with wildcards in different segments should be valid" + ); + // Test invalid patterns - assertEquals(isValidUrlGlobPattern(''), false, 'Empty string should be invalid'); - assertEquals(isValidUrlGlobPattern('//double/slash'), false, 'Path with double slash should be invalid'); - assertEquals(isValidUrlGlobPattern('path//end'), false, 'Path with double slash in the middle should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid'), false, 'Path with invalid characters should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid|char'), false, 'Path with invalid pipe character should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid"char'), false, 'Path with invalid quote character should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid`char'), false, 'Path with invalid backtick character should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid^char'), false, 'Path with invalid caret character should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid\\char'), false, 'Path with invalid backslash character should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid[char]'), false, 'Path with invalid square brackets should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid{char}'), false, 'Path with invalid curly braces should be invalid'); - + assertEquals( + isValidUrlGlobPattern(""), + false, + "Empty string should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("//double/slash"), + false, + "Path with double slash should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("path//end"), + false, + "Path with double slash in the middle should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid"), + false, + "Path with invalid characters should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid|char"), + false, + "Path with invalid pipe character should be invalid" + ); + assertEquals( + isValidUrlGlobPattern('invalid"char'), + false, + "Path with invalid quote character should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid`char"), + false, + "Path with invalid backtick character should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid^char"), + false, + "Path with invalid caret character should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid\\char"), + false, + "Path with invalid backslash character should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid[char]"), + false, + "Path with invalid square brackets should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid{char}"), + false, + "Path with invalid curly braces should be invalid" + ); + // Test invalid percent encoding - assertEquals(isValidUrlGlobPattern('invalid%2'), false, 'Path with incomplete percent encoding should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid%GZ'), false, 'Path with invalid hex in percent encoding should be invalid'); - assertEquals(isValidUrlGlobPattern('invalid%'), false, 'Path with isolated percent sign should be invalid'); - - console.log('All tests passed!'); + assertEquals( + isValidUrlGlobPattern("invalid%2"), + false, + "Path with incomplete percent encoding should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid%GZ"), + false, + "Path with invalid hex in percent encoding should be invalid" + ); + assertEquals( + isValidUrlGlobPattern("invalid%"), + false, + "Path with isolated percent sign should be invalid" + ); + + console.log("All tests passed!"); } // Run all tests try { runTests(); } catch (error) { - console.error('Test failed:', error); -} \ No newline at end of file + console.error("Test failed:", error); +} diff --git a/server/lib/validators.ts b/server/lib/validators.ts index 5bdd7a14..b1efe8b3 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -2,7 +2,9 @@ import z from "zod"; import ipaddr from "ipaddr.js"; export function isValidCIDR(cidr: string): boolean { - return z.cidrv4().safeParse(cidr).success || z.cidrv6().safeParse(cidr).success; + return ( + z.cidrv4().safeParse(cidr).success || z.cidrv6().safeParse(cidr).success + ); } export function isValidIP(ip: string): boolean { @@ -69,11 +71,11 @@ export function isUrlValid(url: string | undefined) { if (!url) return true; // the link is optional in the schema so if it's empty it's valid var pattern = new RegExp( "^(https?:\\/\\/)?" + // protocol - "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name - "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address - "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path - "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string - "(\\#[-a-z\\d_]*)?$", + "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name + "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address + "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path + "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string + "(\\#[-a-z\\d_]*)?$", "i" ); return !!pattern.test(url); @@ -168,14 +170,14 @@ export function validateHeaders(headers: string): boolean { } export function isSecondLevelDomain(domain: string): boolean { - if (!domain || typeof domain !== 'string') { + if (!domain || typeof domain !== "string") { return false; } const trimmedDomain = domain.trim().toLowerCase(); // Split into parts - const parts = trimmedDomain.split('.'); + const parts = trimmedDomain.split("."); // Should have exactly 2 parts for a second-level domain (e.g., "example.com") if (parts.length !== 2) { diff --git a/server/middlewares/formatError.ts b/server/middlewares/formatError.ts index e96ff296..1e94c1f5 100644 --- a/server/middlewares/formatError.ts +++ b/server/middlewares/formatError.ts @@ -20,6 +20,6 @@ export const errorHandlerMiddleware: ErrorRequestHandler = ( error: true, message: error.message || "Internal Server Error", status: statusCode, - stack: process.env.ENVIRONMENT === "prod" ? null : error.stack, + stack: process.env.ENVIRONMENT === "prod" ? null : error.stack }); }; diff --git a/server/middlewares/getUserOrgs.ts b/server/middlewares/getUserOrgs.ts index 4d042307..d7905700 100644 --- a/server/middlewares/getUserOrgs.ts +++ b/server/middlewares/getUserOrgs.ts @@ -8,13 +8,13 @@ import HttpCode from "@server/types/HttpCode"; export async function getUserOrgs( req: Request, res: Response, - next: NextFunction, + next: NextFunction ) { const userId = req.user?.userId; // Assuming you have user information in the request if (!userId) { return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated"), + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") ); } @@ -22,7 +22,7 @@ export async function getUserOrgs( const userOrganizations = await db .select({ orgId: userOrgs.orgId, - roleId: userOrgs.roleId, + roleId: userOrgs.roleId }) .from(userOrgs) .where(eq(userOrgs.userId, userId)); @@ -38,8 +38,8 @@ export async function getUserOrgs( next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "Error retrieving user organizations", - ), + "Error retrieving user organizations" + ) ); } } diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index d44eb5a3..2e2e8ff0 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -12,4 +12,4 @@ export * from "./verifyAccessTokenAccess"; export * from "./verifyApiKeyIsRoot"; export * from "./verifyApiKeyApiKeyAccess"; export * from "./verifyApiKeyClientAccess"; -export * from "./verifyApiKeySiteResourceAccess"; \ No newline at end of file +export * from "./verifyApiKeySiteResourceAccess"; diff --git a/server/middlewares/integration/verifyAccessTokenAccess.ts b/server/middlewares/integration/verifyAccessTokenAccess.ts index f5ae8746..c9a84f18 100644 --- a/server/middlewares/integration/verifyAccessTokenAccess.ts +++ b/server/middlewares/integration/verifyAccessTokenAccess.ts @@ -97,7 +97,6 @@ export async function verifyApiKeyAccessTokenAccess( ); } - return next(); } catch (e) { return next( diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts index ad5b7fc4..48fbbf87 100644 --- a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts +++ b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts @@ -11,7 +11,7 @@ export async function verifyApiKeyApiKeyAccess( next: NextFunction ) { try { - const {apiKey: callerApiKey } = req; + const { apiKey: callerApiKey } = req; const apiKeyId = req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; @@ -44,7 +44,10 @@ export async function verifyApiKeyApiKeyAccess( .select() .from(apiKeyOrg) .where( - and(eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), eq(apiKeyOrg.orgId, orgId)) + and( + eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) ) .limit(1); diff --git a/server/middlewares/integration/verifyApiKeySetResourceClients.ts b/server/middlewares/integration/verifyApiKeySetResourceClients.ts index cbcb33ae..704f3ef5 100644 --- a/server/middlewares/integration/verifyApiKeySetResourceClients.ts +++ b/server/middlewares/integration/verifyApiKeySetResourceClients.ts @@ -11,9 +11,12 @@ export async function verifyApiKeySetResourceClients( next: NextFunction ) { const apiKey = req.apiKey; - const singleClientId = req.params.clientId || req.body.clientId || req.query.clientId; + const singleClientId = + req.params.clientId || req.body.clientId || req.query.clientId; const { clientIds } = req.body; - const allClientIds = clientIds || (singleClientId ? [parseInt(singleClientId as string)] : []); + const allClientIds = + clientIds || + (singleClientId ? [parseInt(singleClientId as string)] : []); if (!apiKey) { return next( @@ -70,4 +73,3 @@ export async function verifyApiKeySetResourceClients( ); } } - diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts index db73d134..0d44aa09 100644 --- a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts +++ b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts @@ -11,7 +11,8 @@ export async function verifyApiKeySetResourceUsers( next: NextFunction ) { const apiKey = req.apiKey; - const singleUserId = req.params.userId || req.body.userId || req.query.userId; + const singleUserId = + req.params.userId || req.body.userId || req.query.userId; const { userIds } = req.body; const allUserIds = userIds || (singleUserId ? [singleUserId] : []); diff --git a/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts b/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts index fb3d8287..1fc11c31 100644 --- a/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts +++ b/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts @@ -38,17 +38,12 @@ export async function verifyApiKeySiteResourceAccess( const [siteResource] = await db .select() .from(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId) - )) + .where(and(eq(siteResources.siteResourceId, siteResourceId))) .limit(1); if (!siteResource) { return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site resource not found" - ) + createHttpError(HttpCode.NOT_FOUND, "Site resource not found") ); } diff --git a/server/middlewares/notFound.ts b/server/middlewares/notFound.ts index 706796c9..8e0ab332 100644 --- a/server/middlewares/notFound.ts +++ b/server/middlewares/notFound.ts @@ -5,7 +5,7 @@ import HttpCode from "@server/types/HttpCode"; export function notFoundMiddleware( req: Request, res: Response, - next: NextFunction, + next: NextFunction ) { if (req.path.startsWith("/api")) { const message = `The requests url is not found - ${req.originalUrl}`; diff --git a/server/middlewares/requestTimeout.ts b/server/middlewares/requestTimeout.ts index 8b5852b7..b0f95a08 100644 --- a/server/middlewares/requestTimeout.ts +++ b/server/middlewares/requestTimeout.ts @@ -1,30 +1,32 @@ -import { Request, Response, NextFunction } from 'express'; -import logger from '@server/logger'; -import createHttpError from 'http-errors'; -import HttpCode from '@server/types/HttpCode'; +import { Request, Response, NextFunction } from "express"; +import logger from "@server/logger"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; export function requestTimeoutMiddleware(timeoutMs: number = 30000) { return (req: Request, res: Response, next: NextFunction) => { // Set a timeout for the request const timeout = setTimeout(() => { if (!res.headersSent) { - logger.error(`Request timeout: ${req.method} ${req.url} from ${req.ip}`); + logger.error( + `Request timeout: ${req.method} ${req.url} from ${req.ip}` + ); return next( createHttpError( HttpCode.REQUEST_TIMEOUT, - 'Request timeout - operation took too long to complete' + "Request timeout - operation took too long to complete" ) ); } }, timeoutMs); // Clear timeout when response finishes - res.on('finish', () => { + res.on("finish", () => { clearTimeout(timeout); }); // Clear timeout when response closes - res.on('close', () => { + res.on("close", () => { clearTimeout(timeout); }); diff --git a/server/middlewares/verifyClientAccess.ts b/server/middlewares/verifyClientAccess.ts index ab65ba2a..d2df38a4 100644 --- a/server/middlewares/verifyClientAccess.ts +++ b/server/middlewares/verifyClientAccess.ts @@ -1,10 +1,11 @@ import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; +import { Client, db } from "@server/db"; import { userOrgs, clients, roleClients, userClients } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; +import logger from "@server/logger"; export async function verifyClientAccess( req: Request, @@ -12,33 +13,51 @@ export async function verifyClientAccess( next: NextFunction ) { const userId = req.user!.userId; // Assuming you have user information in the request - const clientId = parseInt( - req.params.clientId || req.body.clientId || req.query.clientId - ); - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - if (isNaN(clientId)) { - return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID")); - } + const clientIdStr = + req.params?.clientId || req.body?.clientId || req.query?.clientId; + const niceId = req.params?.niceId || req.body?.niceId || req.query?.niceId; + const orgId = req.params?.orgId || req.body?.orgId || req.query?.orgId; try { - // Get the client - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + let client: Client | null = null; + + if (niceId && orgId) { + const [clientRes] = await db + .select() + .from(clients) + .where( + and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)) + ) + .limit(1); + client = clientRes; + } else { + const clientId = parseInt(clientIdStr); + if (isNaN(clientId)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID") + ); + } + + // Get the client + const [clientRes] = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + client = clientRes; + } if (!client) { return next( createHttpError( HttpCode.NOT_FOUND, - `Client with ID ${clientId} not found` + `Client with ID ${niceId || clientIdStr} not found` ) ); } @@ -47,12 +66,12 @@ export async function verifyClientAccess( return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - `Client with ID ${clientId} does not have an organization ID` + `Client with ID ${niceId || clientIdStr} does not have an organization ID` ) ); } - if (!req.userOrg) { + if (!req.userOrg || req.userOrg?.orgId !== client.orgId) { // Get user's role ID in the organization const userOrgRole = await db .select() @@ -104,7 +123,7 @@ export async function verifyClientAccess( .from(roleClients) .where( and( - eq(roleClients.clientId, clientId), + eq(roleClients.clientId, client.clientId), eq(roleClients.roleId, userOrgRoleId) ) ) @@ -122,7 +141,7 @@ export async function verifyClientAccess( .where( and( eq(userClients.userId, userId), - eq(userClients.clientId, clientId) + eq(userClients.clientId, client.clientId) ) ) .limit(1); @@ -140,6 +159,7 @@ export async function verifyClientAccess( ) ); } catch (error) { + logger.error("Error verifying client access", error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, diff --git a/server/middlewares/verifyOrgAccess.ts b/server/middlewares/verifyOrgAccess.ts index c5224ae5..729766ab 100644 --- a/server/middlewares/verifyOrgAccess.ts +++ b/server/middlewares/verifyOrgAccess.ts @@ -5,7 +5,6 @@ import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; -import logger from "@server/logger"; export async function verifyOrgAccess( req: Request, @@ -68,6 +67,7 @@ export async function verifyOrgAccess( // User has access, attach the user's role to the request for potential future use req.userOrgRoleId = req.userOrg.roleId; req.userOrgId = orgId; + return next(); } catch (e) { return next( diff --git a/server/middlewares/verifyResourceAccess.ts b/server/middlewares/verifyResourceAccess.ts index 9b0763ab..2ae591ee 100644 --- a/server/middlewares/verifyResourceAccess.ts +++ b/server/middlewares/verifyResourceAccess.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; +import { db, Resource } from "@server/db"; import { resources, userOrgs, userResources, roleResources } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; @@ -12,36 +12,56 @@ export async function verifyResourceAccess( next: NextFunction ) { const userId = req.user!.userId; - const resourceId = - req.params.resourceId || req.body.resourceId || req.query.resourceId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } + const resourceIdStr = + req.params?.resourceId || req.body?.resourceId || req.query?.resourceId; + const niceId = req.params?.niceId || req.body?.niceId || req.query?.niceId; + const orgId = req.params?.orgId || req.body?.orgId || req.query?.orgId; try { - const resource = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } - if (resource.length === 0) { + let resource: Resource | null = null; + + if (orgId && niceId) { + const [resourceRes] = await db + .select() + .from(resources) + .where( + and( + eq(resources.niceId, niceId), + eq(resources.orgId, orgId) + ) + ) + .limit(1); + resource = resourceRes; + } else { + const resourceId = parseInt(resourceIdStr); + const [resourceRes] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + resource = resourceRes; + } + + if (!resource) { return next( createHttpError( HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` + `Resource with ID ${resourceIdStr || niceId} not found` ) ); } - if (!resource[0].orgId) { + if (!resource.orgId) { return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - `Resource with ID ${resourceId} does not have an organization ID` + `Resource with ID ${resourceIdStr || niceId} does not have an organization ID` ) ); } @@ -53,14 +73,14 @@ export async function verifyResourceAccess( .where( and( eq(userOrgs.userId, userId), - eq(userOrgs.orgId, resource[0].orgId) + eq(userOrgs.orgId, resource.orgId) ) ) .limit(1); req.userOrg = userOrgRole[0]; } - if (!req.userOrg) { + if (!req.userOrg || req.userOrg?.orgId !== resource.orgId) { return next( createHttpError( HttpCode.FORBIDDEN, @@ -89,14 +109,14 @@ export async function verifyResourceAccess( const userOrgRoleId = req.userOrg.roleId; req.userOrgRoleId = userOrgRoleId; - req.userOrgId = resource[0].orgId; + req.userOrgId = resource.orgId; const roleResourceAccess = await db .select() .from(roleResources) .where( and( - eq(roleResources.resourceId, resourceId), + eq(roleResources.resourceId, resource.resourceId), eq(roleResources.roleId, userOrgRoleId) ) ) @@ -112,7 +132,7 @@ export async function verifyResourceAccess( .where( and( eq(userResources.userId, userId), - eq(userResources.resourceId, resourceId) + eq(userResources.resourceId, resource.resourceId) ) ) .limit(1); diff --git a/server/middlewares/verifySiteAccess.ts b/server/middlewares/verifySiteAccess.ts index 06f06a93..98858cfb 100644 --- a/server/middlewares/verifySiteAccess.ts +++ b/server/middlewares/verifySiteAccess.ts @@ -1,10 +1,9 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { sites, userOrgs, userSites, roleSites, roles } from "@server/db"; +import { sites, Site, userOrgs, userSites, roleSites, roles } from "@server/db"; import { and, eq, or } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; export async function verifySiteAccess( @@ -13,9 +12,10 @@ export async function verifySiteAccess( next: NextFunction ) { const userId = req.user!.userId; // Assuming you have user information in the request - const siteId = parseInt( - req.params.siteId || req.body.siteId || req.query.siteId - ); + const siteIdStr = + req.params?.siteId || req.body?.siteId || req.query?.siteId; + const niceId = req.params?.niceId || req.body?.niceId || req.query?.niceId; + const orgId = req.params?.orgId || req.body?.orgId || req.query?.orgId; if (!userId) { return next( @@ -23,32 +23,49 @@ export async function verifySiteAccess( ); } - if (isNaN(siteId)) { - return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID")); - } - try { - // Get the site - const site = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); + let site: Site | null = null; - if (site.length === 0) { + if (niceId && orgId) { + const [siteRes] = await db + .select() + .from(sites) + .where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId))) + .limit(1); + + site = siteRes; + } else { + const siteId = parseInt(siteIdStr); + if (isNaN(siteId)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID") + ); + } + + // Get the site + const [siteRes] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + + site = siteRes; + } + + if (!site) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` + `Site with ID ${siteIdStr || niceId} not found` ) ); } - if (!site[0].orgId) { + if (!site.orgId) { return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - `Site with ID ${siteId} does not have an organization ID` + `Site with ID ${siteIdStr} does not have an organization ID` ) ); } @@ -61,14 +78,14 @@ export async function verifySiteAccess( .where( and( eq(userOrgs.userId, userId), - eq(userOrgs.orgId, site[0].orgId) + eq(userOrgs.orgId, site.orgId) ) ) .limit(1); req.userOrg = userOrgRole[0]; } - if (!req.userOrg) { + if (!req.userOrg || req.userOrg?.orgId !== site.orgId) { return next( createHttpError( HttpCode.FORBIDDEN, @@ -97,7 +114,7 @@ export async function verifySiteAccess( const userOrgRoleId = req.userOrg.roleId; req.userOrgRoleId = userOrgRoleId; - req.userOrgId = site[0].orgId; + req.userOrgId = site.orgId; // Check role-based site access first const roleSiteAccess = await db @@ -105,7 +122,7 @@ export async function verifySiteAccess( .from(roleSites) .where( and( - eq(roleSites.siteId, siteId), + eq(roleSites.siteId, site.siteId), eq(roleSites.roleId, userOrgRoleId) ) ) @@ -121,7 +138,10 @@ export async function verifySiteAccess( .select() .from(userSites) .where( - and(eq(userSites.userId, userId), eq(userSites.siteId, siteId)) + and( + eq(userSites.userId, userId), + eq(userSites.siteId, site.siteId) + ) ) .limit(1); diff --git a/server/nextServer.ts b/server/nextServer.ts index 5302b9c8..b862a699 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -9,7 +9,10 @@ const nextPort = config.getRawConfig().server.next_port; export async function createNextServer() { // const app = next({ dev }); - const app = next({ dev: process.env.ENVIRONMENT !== "prod", turbopack: true }); + const app = next({ + dev: process.env.ENVIRONMENT !== "prod", + turbopack: true + }); const handle = app.getRequestHandler(); await app.prepare(); diff --git a/server/private/auth/sessions/remoteExitNode.ts b/server/private/auth/sessions/remoteExitNode.ts index fbb2ae1f..da1fb1aa 100644 --- a/server/private/auth/sessions/remoteExitNode.ts +++ b/server/private/auth/sessions/remoteExitNode.ts @@ -11,11 +11,14 @@ * This file is not licensed under the AGPLv3. */ -import { - encodeHexLowerCase, -} from "@oslojs/encoding"; +import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { RemoteExitNode, remoteExitNodes, remoteExitNodeSessions, RemoteExitNodeSession } from "@server/db"; +import { + RemoteExitNode, + remoteExitNodes, + remoteExitNodeSessions, + RemoteExitNodeSession +} from "@server/db"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; @@ -23,30 +26,39 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30; export async function createRemoteExitNodeSession( token: string, - remoteExitNodeId: string, + remoteExitNodeId: string ): Promise { const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), + sha256(new TextEncoder().encode(token)) ); const session: RemoteExitNodeSession = { sessionId: sessionId, remoteExitNodeId, - expiresAt: new Date(Date.now() + EXPIRES).getTime(), + expiresAt: new Date(Date.now() + EXPIRES).getTime() }; await db.insert(remoteExitNodeSessions).values(session); return session; } export async function validateRemoteExitNodeSessionToken( - token: string, + token: string ): Promise { const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), + sha256(new TextEncoder().encode(token)) ); const result = await db - .select({ remoteExitNode: remoteExitNodes, session: remoteExitNodeSessions }) + .select({ + remoteExitNode: remoteExitNodes, + session: remoteExitNodeSessions + }) .from(remoteExitNodeSessions) - .innerJoin(remoteExitNodes, eq(remoteExitNodeSessions.remoteExitNodeId, remoteExitNodes.remoteExitNodeId)) + .innerJoin( + remoteExitNodes, + eq( + remoteExitNodeSessions.remoteExitNodeId, + remoteExitNodes.remoteExitNodeId + ) + ) .where(eq(remoteExitNodeSessions.sessionId, sessionId)); if (result.length < 1) { return { session: null, remoteExitNode: null }; @@ -58,26 +70,32 @@ export async function validateRemoteExitNodeSessionToken( .where(eq(remoteExitNodeSessions.sessionId, session.sessionId)); return { session: null, remoteExitNode: null }; } - if (Date.now() >= session.expiresAt - (EXPIRES / 2)) { - session.expiresAt = new Date( - Date.now() + EXPIRES, - ).getTime(); + if (Date.now() >= session.expiresAt - EXPIRES / 2) { + session.expiresAt = new Date(Date.now() + EXPIRES).getTime(); await db .update(remoteExitNodeSessions) .set({ - expiresAt: session.expiresAt, + expiresAt: session.expiresAt }) .where(eq(remoteExitNodeSessions.sessionId, session.sessionId)); } return { session, remoteExitNode }; } -export async function invalidateRemoteExitNodeSession(sessionId: string): Promise { - await db.delete(remoteExitNodeSessions).where(eq(remoteExitNodeSessions.sessionId, sessionId)); +export async function invalidateRemoteExitNodeSession( + sessionId: string +): Promise { + await db + .delete(remoteExitNodeSessions) + .where(eq(remoteExitNodeSessions.sessionId, sessionId)); } -export async function invalidateAllRemoteExitNodeSessions(remoteExitNodeId: string): Promise { - await db.delete(remoteExitNodeSessions).where(eq(remoteExitNodeSessions.remoteExitNodeId, remoteExitNodeId)); +export async function invalidateAllRemoteExitNodeSessions( + remoteExitNodeId: string +): Promise { + await db + .delete(remoteExitNodeSessions) + .where(eq(remoteExitNodeSessions.remoteExitNodeId, remoteExitNodeId)); } export type SessionValidationResult = diff --git a/server/private/cleanup.ts b/server/private/cleanup.ts index 8bf5ea3d..e9b30527 100644 --- a/server/private/cleanup.ts +++ b/server/private/cleanup.ts @@ -25,4 +25,4 @@ export async function initCleanup() { // Handle process termination process.on("SIGTERM", () => cleanup()); process.on("SIGINT", () => cleanup()); -} \ No newline at end of file +} diff --git a/server/private/lib/billing/index.ts b/server/private/lib/billing/index.ts index 13ca3761..c2b77d5f 100644 --- a/server/private/lib/billing/index.ts +++ b/server/private/lib/billing/index.ts @@ -12,4 +12,4 @@ */ export * from "./getOrgTierData"; -export * from "./createCustomer"; \ No newline at end of file +export * from "./createCustomer"; diff --git a/server/private/lib/certificates.ts b/server/private/lib/certificates.ts index ec4b73ee..06571cac 100644 --- a/server/private/lib/certificates.ts +++ b/server/private/lib/certificates.ts @@ -55,7 +55,6 @@ export async function getValidCertificatesForDomains( domains: Set, useCache: boolean = true ): Promise> { - loadEncryptData(); // Ensure encryption key is loaded const finalResults: CertificateResult[] = []; diff --git a/server/private/lib/checkOrgAccessPolicy.ts b/server/private/lib/checkOrgAccessPolicy.ts index 2137cd72..7a78803d 100644 --- a/server/private/lib/checkOrgAccessPolicy.ts +++ b/server/private/lib/checkOrgAccessPolicy.ts @@ -12,14 +12,7 @@ */ import { build } from "@server/build"; -import { - db, - Org, - orgs, - ResourceSession, - sessions, - users -} from "@server/db"; +import { db, Org, orgs, ResourceSession, sessions, users } from "@server/db"; import { getOrgTierData } from "#private/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import license from "#private/license/license"; diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts index 5337ff3f..97baf1e0 100644 --- a/server/private/lib/config.ts +++ b/server/private/lib/config.ts @@ -83,9 +83,6 @@ export class PrivateConfig { ? this.rawPrivateConfig.branding?.logo?.navbar?.height.toString() : undefined; - process.env.BRANDING_FAVICON_PATH = - this.rawPrivateConfig.branding?.favicon_path; - process.env.BRANDING_APP_NAME = this.rawPrivateConfig.branding?.app_name || "Pangolin"; @@ -95,13 +92,9 @@ export class PrivateConfig { ); } - process.env.LOGIN_PAGE_TITLE_TEXT = - this.rawPrivateConfig.branding?.login_page?.title_text || ""; process.env.LOGIN_PAGE_SUBTITLE_TEXT = this.rawPrivateConfig.branding?.login_page?.subtitle_text || ""; - process.env.SIGNUP_PAGE_TITLE_TEXT = - this.rawPrivateConfig.branding?.signup_page?.title_text || ""; process.env.SIGNUP_PAGE_SUBTITLE_TEXT = this.rawPrivateConfig.branding?.signup_page?.subtitle_text || ""; diff --git a/server/private/lib/exitNodes/exitNodeComms.ts b/server/private/lib/exitNodes/exitNodeComms.ts index 20c850a1..faf1153f 100644 --- a/server/private/lib/exitNodes/exitNodeComms.ts +++ b/server/private/lib/exitNodes/exitNodeComms.ts @@ -66,7 +66,9 @@ export async function sendToExitNode( // logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`); if (exitNode.name == config.getRawConfig().gerbil.exit_node_name) { - hostname = privateConfig.getRawPrivateConfig().gerbil.local_exit_node_reachable_at; + hostname = + privateConfig.getRawPrivateConfig().gerbil + .local_exit_node_reachable_at; } if (!hostname) { diff --git a/server/private/lib/exitNodes/exitNodes.ts b/server/private/lib/exitNodes/exitNodes.ts index 77149bb0..556fdcf7 100644 --- a/server/private/lib/exitNodes/exitNodes.ts +++ b/server/private/lib/exitNodes/exitNodes.ts @@ -44,43 +44,53 @@ async function checkExitNodeOnlineStatus( const delayBetweenAttempts = 100; // 100ms delay between starting each attempt // Create promises for all attempts with staggered delays - const attemptPromises = Array.from({ length: maxAttempts }, async (_, index) => { - const attemptNumber = index + 1; - - // Add delay before each attempt (except the first) - if (index > 0) { - await new Promise((resolve) => setTimeout(resolve, delayBetweenAttempts * index)); - } + const attemptPromises = Array.from( + { length: maxAttempts }, + async (_, index) => { + const attemptNumber = index + 1; - try { - const response = await axios.get(`http://${endpoint}/ping`, { - timeout: timeoutMs, - validateStatus: (status) => status === 200 - }); - - if (response.status === 200) { - logger.debug( - `Exit node ${endpoint} is online (attempt ${attemptNumber}/${maxAttempts})` + // Add delay before each attempt (except the first) + if (index > 0) { + await new Promise((resolve) => + setTimeout(resolve, delayBetweenAttempts * index) ); - return { success: true, attemptNumber }; } - return { success: false, attemptNumber, error: 'Non-200 status' }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - logger.debug( - `Exit node ${endpoint} ping failed (attempt ${attemptNumber}/${maxAttempts}): ${errorMessage}` - ); - return { success: false, attemptNumber, error: errorMessage }; + + try { + const response = await axios.get(`http://${endpoint}/ping`, { + timeout: timeoutMs, + validateStatus: (status) => status === 200 + }); + + if (response.status === 200) { + logger.debug( + `Exit node ${endpoint} is online (attempt ${attemptNumber}/${maxAttempts})` + ); + return { success: true, attemptNumber }; + } + return { + success: false, + attemptNumber, + error: "Non-200 status" + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + logger.debug( + `Exit node ${endpoint} ping failed (attempt ${attemptNumber}/${maxAttempts}): ${errorMessage}` + ); + return { success: false, attemptNumber, error: errorMessage }; + } } - }); + ); try { // Wait for the first successful response or all to fail const results = await Promise.allSettled(attemptPromises); - + // Check if any attempt succeeded for (const result of results) { - if (result.status === 'fulfilled' && result.value.success) { + if (result.status === "fulfilled" && result.value.success) { return true; } } @@ -137,7 +147,11 @@ export async function verifyExitNodeOrgAccess( return { hasAccess: false, exitNode }; } -export async function listExitNodes(orgId: string, filterOnline = false, noCloud = false) { +export async function listExitNodes( + orgId: string, + filterOnline = false, + noCloud = false +) { const allExitNodes = await db .select({ exitNodeId: exitNodes.exitNodeId, @@ -166,7 +180,10 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud eq(exitNodes.type, "gerbil"), or( // only choose nodes that are in the same region - eq(exitNodes.region, config.getRawPrivateConfig().app.region), + eq( + exitNodes.region, + config.getRawPrivateConfig().app.region + ), isNull(exitNodes.region) // or for enterprise where region is not set ) ), @@ -191,7 +208,7 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud // let online: boolean; // if (filterOnline && node.type == "remoteExitNode") { // try { - // const isActuallyOnline = await checkExitNodeOnlineStatus( + // const isActuallyOnline = await checkExitNodeOnlineStatus( // node.endpoint // ); @@ -225,7 +242,8 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud node.type === "remoteExitNode" && (!filterOnline || node.online) ); const gerbilExitNodes = allExitNodes.filter( - (node) => node.type === "gerbil" && (!filterOnline || node.online) && !noCloud + (node) => + node.type === "gerbil" && (!filterOnline || node.online) && !noCloud ); // THIS PROVIDES THE FALL @@ -334,7 +352,11 @@ export function selectBestExitNode( return fallbackNode; } -export async function checkExitNodeOrg(exitNodeId: number, orgId: string, trx: Transaction | typeof db = db) { +export async function checkExitNodeOrg( + exitNodeId: number, + orgId: string, + trx: Transaction | typeof db = db +) { const [exitNodeOrg] = await trx .select() .from(exitNodeOrgs) diff --git a/server/private/lib/exitNodes/index.ts b/server/private/lib/exitNodes/index.ts index 098a0580..00113b64 100644 --- a/server/private/lib/exitNodes/index.ts +++ b/server/private/lib/exitNodes/index.ts @@ -12,4 +12,4 @@ */ export * from "./exitNodeComms"; -export * from "./exitNodes"; \ No newline at end of file +export * from "./exitNodes"; diff --git a/server/private/lib/lock.ts b/server/private/lib/lock.ts index 4a12063b..08496f65 100644 --- a/server/private/lib/lock.ts +++ b/server/private/lib/lock.ts @@ -177,7 +177,9 @@ export class LockManager { const exists = value !== null; const ownedByMe = exists && - value!.startsWith(`${config.getRawConfig().gerbil.exit_node_name}:`); + value!.startsWith( + `${config.getRawConfig().gerbil.exit_node_name}:` + ); const owner = exists ? value!.split(":")[0] : undefined; return { diff --git a/server/private/lib/logAccessAudit.ts b/server/private/lib/logAccessAudit.ts index 72f81069..98eaa6ec 100644 --- a/server/private/lib/logAccessAudit.ts +++ b/server/private/lib/logAccessAudit.ts @@ -16,6 +16,7 @@ import { getCountryCodeForIp } from "@server/lib/geoip"; import logger from "@server/logger"; import { and, eq, lt } from "drizzle-orm"; import cache from "@server/lib/cache"; +import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; async function getAccessDays(orgId: string): Promise { // check cache first @@ -47,9 +48,7 @@ async function getAccessDays(orgId: string): Promise { } export async function cleanUpOldLogs(orgId: string, retentionDays: number) { - const now = Math.floor(Date.now() / 1000); - - const cutoffTimestamp = now - retentionDays * 24 * 60 * 60; + const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); try { await db diff --git a/server/private/lib/rateLimit.test.ts b/server/private/lib/rateLimit.test.ts index 59952c8c..96adf082 100644 --- a/server/private/lib/rateLimit.test.ts +++ b/server/private/lib/rateLimit.test.ts @@ -14,15 +14,15 @@ // Simple test file for the rate limit service with Redis // Run with: npx ts-node rateLimitService.test.ts -import { RateLimitService } from './rateLimit'; +import { RateLimitService } from "./rateLimit"; function generateClientId() { - return 'client-' + Math.random().toString(36).substring(2, 15); + return "client-" + Math.random().toString(36).substring(2, 15); } async function runTests() { - console.log('Starting Rate Limit Service Tests...\n'); - + console.log("Starting Rate Limit Service Tests...\n"); + const rateLimitService = new RateLimitService(); let testsPassed = 0; let testsTotal = 0; @@ -47,36 +47,54 @@ async function runTests() { } // Test 1: Basic rate limiting - await test('Should allow requests under the limit', async () => { + await test("Should allow requests under the limit", async () => { const clientId = generateClientId(); const maxRequests = 5; for (let i = 0; i < maxRequests - 1; i++) { - const result = await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); + const result = await rateLimitService.checkRateLimit( + clientId, + undefined, + maxRequests + ); assert(!result.isLimited, `Request ${i + 1} should be allowed`); - assert(result.totalHits === i + 1, `Expected ${i + 1} hits, got ${result.totalHits}`); + assert( + result.totalHits === i + 1, + `Expected ${i + 1} hits, got ${result.totalHits}` + ); } }); // Test 2: Rate limit blocking - await test('Should block requests over the limit', async () => { + await test("Should block requests over the limit", async () => { const clientId = generateClientId(); const maxRequests = 30; // Use up all allowed requests for (let i = 0; i < maxRequests - 1; i++) { - const result = await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); + const result = await rateLimitService.checkRateLimit( + clientId, + undefined, + maxRequests + ); assert(!result.isLimited, `Request ${i + 1} should be allowed`); } // Next request should be blocked - const blockedResult = await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); - assert(blockedResult.isLimited, 'Request should be blocked'); - assert(blockedResult.reason === 'global', 'Should be blocked for global reason'); + const blockedResult = await rateLimitService.checkRateLimit( + clientId, + undefined, + maxRequests + ); + assert(blockedResult.isLimited, "Request should be blocked"); + assert( + blockedResult.reason === "global", + "Should be blocked for global reason" + ); }); // Test 3: Message type limits - await test('Should handle message type limits', async () => { + await test("Should handle message type limits", async () => { const clientId = generateClientId(); const globalMax = 10; const messageTypeMax = 2; @@ -84,54 +102,64 @@ async function runTests() { // Send messages of type 'ping' up to the limit for (let i = 0; i < messageTypeMax - 1; i++) { const result = await rateLimitService.checkRateLimit( - clientId, - 'ping', - globalMax, + clientId, + "ping", + globalMax, messageTypeMax ); - assert(!result.isLimited, `Ping message ${i + 1} should be allowed`); + assert( + !result.isLimited, + `Ping message ${i + 1} should be allowed` + ); } // Next 'ping' should be blocked const blockedResult = await rateLimitService.checkRateLimit( - clientId, - 'ping', - globalMax, + clientId, + "ping", + globalMax, messageTypeMax ); - assert(blockedResult.isLimited, 'Ping message should be blocked'); - assert(blockedResult.reason === 'message_type:ping', 'Should be blocked for message type'); + assert(blockedResult.isLimited, "Ping message should be blocked"); + assert( + blockedResult.reason === "message_type:ping", + "Should be blocked for message type" + ); // Other message types should still work const otherResult = await rateLimitService.checkRateLimit( - clientId, - 'pong', - globalMax, + clientId, + "pong", + globalMax, messageTypeMax ); - assert(!otherResult.isLimited, 'Pong message should be allowed'); + assert(!otherResult.isLimited, "Pong message should be allowed"); }); // Test 4: Reset functionality - await test('Should reset client correctly', async () => { + await test("Should reset client correctly", async () => { const clientId = generateClientId(); const maxRequests = 3; // Use up some requests await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); - await rateLimitService.checkRateLimit(clientId, 'test', maxRequests); + await rateLimitService.checkRateLimit(clientId, "test", maxRequests); // Reset the client await rateLimitService.resetKey(clientId); // Should be able to make fresh requests - const result = await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); - assert(!result.isLimited, 'Request after reset should be allowed'); - assert(result.totalHits === 1, 'Should have 1 hit after reset'); + const result = await rateLimitService.checkRateLimit( + clientId, + undefined, + maxRequests + ); + assert(!result.isLimited, "Request after reset should be allowed"); + assert(result.totalHits === 1, "Should have 1 hit after reset"); }); // Test 5: Different clients are independent - await test('Should handle different clients independently', async () => { + await test("Should handle different clients independently", async () => { const client1 = generateClientId(); const client2 = generateClientId(); const maxRequests = 2; @@ -139,43 +167,62 @@ async function runTests() { // Client 1 uses up their limit await rateLimitService.checkRateLimit(client1, undefined, maxRequests); await rateLimitService.checkRateLimit(client1, undefined, maxRequests); - const client1Blocked = await rateLimitService.checkRateLimit(client1, undefined, maxRequests); - assert(client1Blocked.isLimited, 'Client 1 should be blocked'); + const client1Blocked = await rateLimitService.checkRateLimit( + client1, + undefined, + maxRequests + ); + assert(client1Blocked.isLimited, "Client 1 should be blocked"); // Client 2 should still be able to make requests - const client2Result = await rateLimitService.checkRateLimit(client2, undefined, maxRequests); - assert(!client2Result.isLimited, 'Client 2 should not be blocked'); - assert(client2Result.totalHits === 1, 'Client 2 should have 1 hit'); + const client2Result = await rateLimitService.checkRateLimit( + client2, + undefined, + maxRequests + ); + assert(!client2Result.isLimited, "Client 2 should not be blocked"); + assert(client2Result.totalHits === 1, "Client 2 should have 1 hit"); }); // Test 6: Decrement functionality - await test('Should decrement correctly', async () => { + await test("Should decrement correctly", async () => { const clientId = generateClientId(); const maxRequests = 5; // Make some requests await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); - let result = await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); - assert(result.totalHits === 3, 'Should have 3 hits before decrement'); + let result = await rateLimitService.checkRateLimit( + clientId, + undefined, + maxRequests + ); + assert(result.totalHits === 3, "Should have 3 hits before decrement"); // Decrement await rateLimitService.decrementRateLimit(clientId); // Next request should reflect the decrement - result = await rateLimitService.checkRateLimit(clientId, undefined, maxRequests); - assert(result.totalHits === 3, 'Should have 3 hits after decrement + increment'); + result = await rateLimitService.checkRateLimit( + clientId, + undefined, + maxRequests + ); + assert( + result.totalHits === 3, + "Should have 3 hits after decrement + increment" + ); }); // Wait a moment for any pending Redis operations - console.log('\nWaiting for Redis sync...'); - await new Promise(resolve => setTimeout(resolve, 1000)); + console.log("\nWaiting for Redis sync..."); + await new Promise((resolve) => setTimeout(resolve, 1000)); // Force sync to test Redis integration - await test('Should sync to Redis', async () => { + await test("Should sync to Redis", async () => { await rateLimitService.forceSyncAllPendingData(); // If this doesn't throw, Redis sync is working - assert(true, 'Redis sync completed'); + assert(true, "Redis sync completed"); }); // Cleanup @@ -185,18 +232,18 @@ async function runTests() { console.log(`\n--- Test Results ---`); console.log(`✅ Passed: ${testsPassed}/${testsTotal}`); console.log(`❌ Failed: ${testsTotal - testsPassed}/${testsTotal}`); - + if (testsPassed === testsTotal) { - console.log('\n🎉 All tests passed!'); + console.log("\n🎉 All tests passed!"); process.exit(0); } else { - console.log('\n💥 Some tests failed!'); + console.log("\n💥 Some tests failed!"); process.exit(1); } } // Run the tests -runTests().catch(error => { - console.error('Test runner error:', error); +runTests().catch((error) => { + console.error("Test runner error:", error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/server/private/lib/rateLimit.ts b/server/private/lib/rateLimit.ts index 6d4ab44d..984d95c6 100644 --- a/server/private/lib/rateLimit.ts +++ b/server/private/lib/rateLimit.ts @@ -40,7 +40,8 @@ interface RateLimitResult { export class RateLimitService { private localRateLimitTracker: Map = new Map(); - private localMessageTypeRateLimitTracker: Map = new Map(); + private localMessageTypeRateLimitTracker: Map = + new Map(); private cleanupInterval: NodeJS.Timeout | null = null; private forceSyncInterval: NodeJS.Timeout | null = null; @@ -68,12 +69,18 @@ export class RateLimitService { return `ratelimit:${clientId}`; } - private getMessageTypeRateLimitKey(clientId: string, messageType: string): string { + private getMessageTypeRateLimitKey( + clientId: string, + messageType: string + ): string { return `ratelimit:${clientId}:${messageType}`; } // Helper function to clean up old timestamp fields from a Redis hash - private async cleanupOldTimestamps(key: string, windowStart: number): Promise { + private async cleanupOldTimestamps( + key: string, + windowStart: number + ): Promise { if (!redisManager.isRedisEnabled()) return; try { @@ -101,10 +108,15 @@ export class RateLimitService { const batch = fieldsToDelete.slice(i, i + batchSize); await client.hdel(key, ...batch); } - logger.debug(`Cleaned up ${fieldsToDelete.length} old timestamp fields from ${key}`); + logger.debug( + `Cleaned up ${fieldsToDelete.length} old timestamp fields from ${key}` + ); } } catch (error) { - logger.error(`Failed to cleanup old timestamps for key ${key}:`, error); + logger.error( + `Failed to cleanup old timestamps for key ${key}:`, + error + ); // Don't throw - cleanup failures shouldn't block rate limiting } } @@ -114,7 +126,8 @@ export class RateLimitService { clientId: string, tracker: RateLimitTracker ): Promise { - if (!redisManager.isRedisEnabled() || tracker.pendingCount === 0) return; + if (!redisManager.isRedisEnabled() || tracker.pendingCount === 0) + return; try { const currentTime = Math.floor(Date.now() / 1000); @@ -132,7 +145,11 @@ export class RateLimitService { const newValue = ( parseInt(currentValue || "0") + tracker.pendingCount ).toString(); - await redisManager.hset(globalKey, currentTime.toString(), newValue); + await redisManager.hset( + globalKey, + currentTime.toString(), + newValue + ); // Set TTL using the client directly - this prevents the key from persisting forever if (redisManager.getClient()) { @@ -145,7 +162,9 @@ export class RateLimitService { tracker.lastSyncedCount = tracker.count; tracker.pendingCount = 0; - logger.debug(`Synced global rate limit to Redis for client ${clientId}`); + logger.debug( + `Synced global rate limit to Redis for client ${clientId}` + ); } catch (error) { logger.error("Failed to sync global rate limit to Redis:", error); } @@ -156,12 +175,16 @@ export class RateLimitService { messageType: string, tracker: RateLimitTracker ): Promise { - if (!redisManager.isRedisEnabled() || tracker.pendingCount === 0) return; + if (!redisManager.isRedisEnabled() || tracker.pendingCount === 0) + return; try { const currentTime = Math.floor(Date.now() / 1000); const windowStart = currentTime - RATE_LIMIT_WINDOW; - const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType); + const messageTypeKey = this.getMessageTypeRateLimitKey( + clientId, + messageType + ); // Clean up old timestamp fields before writing await this.cleanupOldTimestamps(messageTypeKey, windowStart); @@ -195,12 +218,17 @@ export class RateLimitService { `Synced message type rate limit to Redis for client ${clientId}, type ${messageType}` ); } catch (error) { - logger.error("Failed to sync message type rate limit to Redis:", error); + logger.error( + "Failed to sync message type rate limit to Redis:", + error + ); } } // Initialize local tracker from Redis data - private async initializeLocalTracker(clientId: string): Promise { + private async initializeLocalTracker( + clientId: string + ): Promise { const currentTime = Math.floor(Date.now() / 1000); const windowStart = currentTime - RATE_LIMIT_WINDOW; @@ -215,14 +243,16 @@ export class RateLimitService { try { const globalKey = this.getRateLimitKey(clientId); - + // Clean up old timestamp fields before reading await this.cleanupOldTimestamps(globalKey, windowStart); - + const globalRateLimitData = await redisManager.hgetall(globalKey); let count = 0; - for (const [timestamp, countStr] of Object.entries(globalRateLimitData)) { + for (const [timestamp, countStr] of Object.entries( + globalRateLimitData + )) { const time = parseInt(timestamp); if (time >= windowStart) { count += parseInt(countStr); @@ -236,7 +266,10 @@ export class RateLimitService { lastSyncedCount: count }; } catch (error) { - logger.error("Failed to initialize global tracker from Redis:", error); + logger.error( + "Failed to initialize global tracker from Redis:", + error + ); return { count: 0, windowStart: currentTime, @@ -263,15 +296,21 @@ export class RateLimitService { } try { - const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType); - + const messageTypeKey = this.getMessageTypeRateLimitKey( + clientId, + messageType + ); + // Clean up old timestamp fields before reading await this.cleanupOldTimestamps(messageTypeKey, windowStart); - - const messageTypeRateLimitData = await redisManager.hgetall(messageTypeKey); + + const messageTypeRateLimitData = + await redisManager.hgetall(messageTypeKey); let count = 0; - for (const [timestamp, countStr] of Object.entries(messageTypeRateLimitData)) { + for (const [timestamp, countStr] of Object.entries( + messageTypeRateLimitData + )) { const time = parseInt(timestamp); if (time >= windowStart) { count += parseInt(countStr); @@ -285,7 +324,10 @@ export class RateLimitService { lastSyncedCount: count }; } catch (error) { - logger.error("Failed to initialize message type tracker from Redis:", error); + logger.error( + "Failed to initialize message type tracker from Redis:", + error + ); return { count: 0, windowStart: currentTime, @@ -327,7 +369,10 @@ export class RateLimitService { isLimited: true, reason: "global", totalHits: globalTracker.count, - resetTime: new Date((globalTracker.windowStart + Math.floor(windowMs / 1000)) * 1000) + resetTime: new Date( + (globalTracker.windowStart + Math.floor(windowMs / 1000)) * + 1000 + ) }; } @@ -339,19 +384,32 @@ export class RateLimitService { // Check message type specific rate limit if messageType is provided if (messageType) { const messageTypeKey = `${clientId}:${messageType}`; - let messageTypeTracker = this.localMessageTypeRateLimitTracker.get(messageTypeKey); + let messageTypeTracker = + this.localMessageTypeRateLimitTracker.get(messageTypeKey); - if (!messageTypeTracker || messageTypeTracker.windowStart < windowStart) { + if ( + !messageTypeTracker || + messageTypeTracker.windowStart < windowStart + ) { // New window or first request for this message type - initialize from Redis if available - messageTypeTracker = await this.initializeMessageTypeTracker(clientId, messageType); + messageTypeTracker = await this.initializeMessageTypeTracker( + clientId, + messageType + ); messageTypeTracker.windowStart = currentTime; - this.localMessageTypeRateLimitTracker.set(messageTypeKey, messageTypeTracker); + this.localMessageTypeRateLimitTracker.set( + messageTypeKey, + messageTypeTracker + ); } // Increment message type counters messageTypeTracker.count++; messageTypeTracker.pendingCount++; - this.localMessageTypeRateLimitTracker.set(messageTypeKey, messageTypeTracker); + this.localMessageTypeRateLimitTracker.set( + messageTypeKey, + messageTypeTracker + ); // Check if message type limit would be exceeded if (messageTypeTracker.count >= messageTypeLimit) { @@ -359,25 +417,38 @@ export class RateLimitService { isLimited: true, reason: `message_type:${messageType}`, totalHits: messageTypeTracker.count, - resetTime: new Date((messageTypeTracker.windowStart + Math.floor(windowMs / 1000)) * 1000) + resetTime: new Date( + (messageTypeTracker.windowStart + + Math.floor(windowMs / 1000)) * + 1000 + ) }; } // Sync to Redis if threshold reached if (messageTypeTracker.pendingCount >= REDIS_SYNC_THRESHOLD) { - this.syncMessageTypeRateLimitToRedis(clientId, messageType, messageTypeTracker); + this.syncMessageTypeRateLimitToRedis( + clientId, + messageType, + messageTypeTracker + ); } } return { isLimited: false, totalHits: globalTracker.count, - resetTime: new Date((globalTracker.windowStart + Math.floor(windowMs / 1000)) * 1000) + resetTime: new Date( + (globalTracker.windowStart + Math.floor(windowMs / 1000)) * 1000 + ) }; } // Decrement function for skipSuccessfulRequests/skipFailedRequests functionality - async decrementRateLimit(clientId: string, messageType?: string): Promise { + async decrementRateLimit( + clientId: string, + messageType?: string + ): Promise { // Decrement global counter const globalTracker = this.localRateLimitTracker.get(clientId); if (globalTracker && globalTracker.count > 0) { @@ -389,7 +460,8 @@ export class RateLimitService { // Decrement message type counter if provided if (messageType) { const messageTypeKey = `${clientId}:${messageType}`; - const messageTypeTracker = this.localMessageTypeRateLimitTracker.get(messageTypeKey); + const messageTypeTracker = + this.localMessageTypeRateLimitTracker.get(messageTypeKey); if (messageTypeTracker && messageTypeTracker.count > 0) { messageTypeTracker.count--; messageTypeTracker.pendingCount--; @@ -401,7 +473,7 @@ export class RateLimitService { async resetKey(clientId: string): Promise { // Remove from local tracking this.localRateLimitTracker.delete(clientId); - + // Remove all message type entries for this client for (const [key] of this.localMessageTypeRateLimitTracker) { if (key.startsWith(`${clientId}:`)) { @@ -417,9 +489,13 @@ export class RateLimitService { // Get all message type keys for this client and delete them const client = redisManager.getClient(); if (client) { - const messageTypeKeys = await client.keys(`ratelimit:${clientId}:*`); + const messageTypeKeys = await client.keys( + `ratelimit:${clientId}:*` + ); if (messageTypeKeys.length > 0) { - await Promise.all(messageTypeKeys.map(key => redisManager.del(key))); + await Promise.all( + messageTypeKeys.map((key) => redisManager.del(key)) + ); } } } @@ -431,7 +507,10 @@ export class RateLimitService { const windowStart = currentTime - RATE_LIMIT_WINDOW; // Clean up global rate limit tracking and sync pending data - for (const [clientId, tracker] of this.localRateLimitTracker.entries()) { + for (const [ + clientId, + tracker + ] of this.localRateLimitTracker.entries()) { if (tracker.windowStart < windowStart) { // Sync any pending data before cleanup if (tracker.pendingCount > 0) { @@ -442,12 +521,19 @@ export class RateLimitService { } // Clean up message type rate limit tracking and sync pending data - for (const [key, tracker] of this.localMessageTypeRateLimitTracker.entries()) { + for (const [ + key, + tracker + ] of this.localMessageTypeRateLimitTracker.entries()) { if (tracker.windowStart < windowStart) { // Sync any pending data before cleanup if (tracker.pendingCount > 0) { const [clientId, messageType] = key.split(":", 2); - await this.syncMessageTypeRateLimitToRedis(clientId, messageType, tracker); + await this.syncMessageTypeRateLimitToRedis( + clientId, + messageType, + tracker + ); } this.localMessageTypeRateLimitTracker.delete(key); } @@ -461,17 +547,27 @@ export class RateLimitService { logger.debug("Force syncing all pending rate limit data to Redis..."); // Sync all pending global rate limits - for (const [clientId, tracker] of this.localRateLimitTracker.entries()) { + for (const [ + clientId, + tracker + ] of this.localRateLimitTracker.entries()) { if (tracker.pendingCount > 0) { await this.syncRateLimitToRedis(clientId, tracker); } } // Sync all pending message type rate limits - for (const [key, tracker] of this.localMessageTypeRateLimitTracker.entries()) { + for (const [ + key, + tracker + ] of this.localMessageTypeRateLimitTracker.entries()) { if (tracker.pendingCount > 0) { const [clientId, messageType] = key.split(":", 2); - await this.syncMessageTypeRateLimitToRedis(clientId, messageType, tracker); + await this.syncMessageTypeRateLimitToRedis( + clientId, + messageType, + tracker + ); } } @@ -504,4 +600,4 @@ export class RateLimitService { } // Export singleton instance -export const rateLimitService = new RateLimitService(); \ No newline at end of file +export const rateLimitService = new RateLimitService(); diff --git a/server/private/lib/rateLimitStore.ts b/server/private/lib/rateLimitStore.ts index 20355125..32495cd2 100644 --- a/server/private/lib/rateLimitStore.ts +++ b/server/private/lib/rateLimitStore.ts @@ -17,7 +17,10 @@ import { MemoryStore, Store } from "express-rate-limit"; import RedisStore from "#private/lib/redisStore"; export function createStore(): Store { - if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) { + if ( + build != "oss" && + privateConfig.getRawPrivateConfig().flags.enable_redis + ) { const rateLimitStore: Store = new RedisStore({ prefix: "api-rate-limit", // Optional: customize Redis key prefix skipFailedRequests: true, // Don't count failed requests diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index 754a5c53..c986e62d 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -115,7 +115,6 @@ export const privateConfigSchema = z.object({ .optional() }) .optional(), - favicon_path: z.string().optional(), footer: z .array( z.object({ @@ -127,14 +126,12 @@ export const privateConfigSchema = z.object({ hide_auth_layout_footer: z.boolean().optional().default(false), login_page: z .object({ - subtitle_text: z.string().optional(), - title_text: z.string().optional() + subtitle_text: z.string().optional() }) .optional(), signup_page: z .object({ - subtitle_text: z.string().optional(), - title_text: z.string().optional() + subtitle_text: z.string().optional() }) .optional(), resource_auth_page: z diff --git a/server/private/lib/redis.ts b/server/private/lib/redis.ts index 324a6a74..6b7826ea 100644 --- a/server/private/lib/redis.ts +++ b/server/private/lib/redis.ts @@ -19,7 +19,7 @@ import { build } from "@server/build"; class RedisManager { public client: Redis | null = null; private writeClient: Redis | null = null; // Master for writes - private readClient: Redis | null = null; // Replica for reads + private readClient: Redis | null = null; // Replica for reads private subscriber: Redis | null = null; private publisher: Redis | null = null; private isEnabled: boolean = false; @@ -46,7 +46,8 @@ class RedisManager { this.isEnabled = false; return; } - this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false; + this.isEnabled = + privateConfig.getRawPrivateConfig().flags.enable_redis || false; if (this.isEnabled) { this.initializeClients(); } @@ -63,15 +64,19 @@ class RedisManager { } private async triggerReconnectionCallbacks(): Promise { - logger.info(`Triggering ${this.reconnectionCallbacks.size} reconnection callbacks`); - - const promises = Array.from(this.reconnectionCallbacks).map(async (callback) => { - try { - await callback(); - } catch (error) { - logger.error("Error in reconnection callback:", error); + logger.info( + `Triggering ${this.reconnectionCallbacks.size} reconnection callbacks` + ); + + const promises = Array.from(this.reconnectionCallbacks).map( + async (callback) => { + try { + await callback(); + } catch (error) { + logger.error("Error in reconnection callback:", error); + } } - }); + ); await Promise.allSettled(promises); } @@ -79,13 +84,17 @@ class RedisManager { private async resubscribeToChannels(): Promise { if (!this.subscriber || this.subscribers.size === 0) return; - logger.info(`Re-subscribing to ${this.subscribers.size} channels after Redis reconnection`); - + logger.info( + `Re-subscribing to ${this.subscribers.size} channels after Redis reconnection` + ); + try { const channels = Array.from(this.subscribers.keys()); if (channels.length > 0) { await this.subscriber.subscribe(...channels); - logger.info(`Successfully re-subscribed to channels: ${channels.join(', ')}`); + logger.info( + `Successfully re-subscribed to channels: ${channels.join(", ")}` + ); } } catch (error) { logger.error("Failed to re-subscribe to channels:", error); @@ -98,7 +107,7 @@ class RedisManager { host: redisConfig.host!, port: redisConfig.port!, password: redisConfig.password, - db: redisConfig.db, + db: redisConfig.db // tls: { // rejectUnauthorized: // redisConfig.tls?.reject_unauthorized || false @@ -112,7 +121,7 @@ class RedisManager { if (!redisConfig.replicas || redisConfig.replicas.length === 0) { return null; } - + // Use the first replica for simplicity // In production, you might want to implement load balancing across replicas const replica = redisConfig.replicas[0]; @@ -120,7 +129,7 @@ class RedisManager { host: replica.host!, port: replica.port!, password: replica.password, - db: replica.db || redisConfig.db, + db: replica.db || redisConfig.db // tls: { // rejectUnauthorized: // replica.tls?.reject_unauthorized || false @@ -133,7 +142,7 @@ class RedisManager { private initializeClients(): void { const masterConfig = this.getRedisConfig(); const replicaConfig = this.getReplicaRedisConfig(); - + this.hasReplicas = replicaConfig !== null; try { @@ -144,7 +153,7 @@ class RedisManager { maxRetriesPerRequest: 3, keepAlive: 30000, connectTimeout: this.connectionTimeout, - commandTimeout: this.commandTimeout, + commandTimeout: this.commandTimeout }); // Initialize replica connection for reads (if available) @@ -155,7 +164,7 @@ class RedisManager { maxRetriesPerRequest: 3, keepAlive: 30000, connectTimeout: this.connectionTimeout, - commandTimeout: this.commandTimeout, + commandTimeout: this.commandTimeout }); } else { // Fallback to master for reads if no replicas @@ -172,7 +181,7 @@ class RedisManager { maxRetriesPerRequest: 3, keepAlive: 30000, connectTimeout: this.connectionTimeout, - commandTimeout: this.commandTimeout, + commandTimeout: this.commandTimeout }); // Subscriber uses replica if available (reads) @@ -182,7 +191,7 @@ class RedisManager { maxRetriesPerRequest: 3, keepAlive: 30000, connectTimeout: this.connectionTimeout, - commandTimeout: this.commandTimeout, + commandTimeout: this.commandTimeout }); // Add reconnection handlers for write client @@ -202,11 +211,14 @@ class RedisManager { logger.info("Redis write client ready"); this.isWriteHealthy = true; this.updateOverallHealth(); - + // Trigger reconnection callbacks when Redis comes back online if (this.isHealthy) { - this.triggerReconnectionCallbacks().catch(error => { - logger.error("Error triggering reconnection callbacks:", error); + this.triggerReconnectionCallbacks().catch((error) => { + logger.error( + "Error triggering reconnection callbacks:", + error + ); }); } }); @@ -233,11 +245,14 @@ class RedisManager { logger.info("Redis read client ready"); this.isReadHealthy = true; this.updateOverallHealth(); - + // Trigger reconnection callbacks when Redis comes back online if (this.isHealthy) { - this.triggerReconnectionCallbacks().catch(error => { - logger.error("Error triggering reconnection callbacks:", error); + this.triggerReconnectionCallbacks().catch((error) => { + logger.error( + "Error triggering reconnection callbacks:", + error + ); }); } }); @@ -298,8 +313,8 @@ class RedisManager { } ); - const setupMessage = this.hasReplicas - ? "Redis clients initialized successfully with replica support" + const setupMessage = this.hasReplicas + ? "Redis clients initialized successfully with replica support" : "Redis clients initialized successfully (single instance)"; logger.info(setupMessage); @@ -313,7 +328,8 @@ class RedisManager { private updateOverallHealth(): void { // Overall health is true if write is healthy and (read is healthy OR we don't have replicas) - this.isHealthy = this.isWriteHealthy && (this.isReadHealthy || !this.hasReplicas); + this.isHealthy = + this.isWriteHealthy && (this.isReadHealthy || !this.hasReplicas); } private async executeWithRetry( @@ -322,49 +338,61 @@ class RedisManager { fallbackOperation?: () => Promise ): Promise { let lastError: Error | null = null; - + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error as Error; - + // If this is the last attempt, try fallback if available if (attempt === this.maxRetries && fallbackOperation) { try { - logger.warn(`${operationName} primary operation failed, trying fallback`); + logger.warn( + `${operationName} primary operation failed, trying fallback` + ); return await fallbackOperation(); } catch (fallbackError) { - logger.error(`${operationName} fallback also failed:`, fallbackError); + logger.error( + `${operationName} fallback also failed:`, + fallbackError + ); throw lastError; } } - + // Don't retry on the last attempt if (attempt === this.maxRetries) { break; } - + // Calculate delay with exponential backoff const delay = Math.min( - this.baseRetryDelay * Math.pow(this.backoffMultiplier, attempt), + this.baseRetryDelay * + Math.pow(this.backoffMultiplier, attempt), this.maxRetryDelay ); - - logger.warn(`${operationName} failed (attempt ${attempt + 1}/${this.maxRetries + 1}), retrying in ${delay}ms:`, error); - + + logger.warn( + `${operationName} failed (attempt ${attempt + 1}/${this.maxRetries + 1}), retrying in ${delay}ms:`, + error + ); + // Wait before retrying - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); } } - - logger.error(`${operationName} failed after ${this.maxRetries + 1} attempts:`, lastError); + + logger.error( + `${operationName} failed after ${this.maxRetries + 1} attempts:`, + lastError + ); throw lastError; } private startHealthMonitoring(): void { if (!this.isEnabled) return; - + // Check health every 30 seconds setInterval(async () => { try { @@ -381,7 +409,7 @@ class RedisManager { private async checkRedisHealth(): Promise { const now = Date.now(); - + // Only check health every 30 seconds if (now - this.lastHealthCheck < this.healthCheckInterval) { return this.isHealthy; @@ -400,24 +428,45 @@ class RedisManager { // Check write client (master) health await Promise.race([ this.writeClient.ping(), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Write client health check timeout')), 2000) + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error("Write client health check timeout") + ), + 2000 + ) ) ]); this.isWriteHealthy = true; // Check read client health if it's different from write client - if (this.hasReplicas && this.readClient && this.readClient !== this.writeClient) { + if ( + this.hasReplicas && + this.readClient && + this.readClient !== this.writeClient + ) { try { await Promise.race([ this.readClient.ping(), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Read client health check timeout')), 2000) + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error( + "Read client health check timeout" + ) + ), + 2000 + ) ) ]); this.isReadHealthy = true; } catch (error) { - logger.error("Redis read client health check failed:", error); + logger.error( + "Redis read client health check failed:", + error + ); this.isReadHealthy = false; } } else { @@ -475,16 +524,13 @@ class RedisManager { if (!this.isRedisEnabled() || !this.writeClient) return false; try { - await this.executeWithRetry( - async () => { - if (ttl) { - await this.writeClient!.setex(key, ttl, value); - } else { - await this.writeClient!.set(key, value); - } - }, - "Redis SET" - ); + await this.executeWithRetry(async () => { + if (ttl) { + await this.writeClient!.setex(key, ttl, value); + } else { + await this.writeClient!.set(key, value); + } + }, "Redis SET"); return true; } catch (error) { logger.error("Redis SET error:", error); @@ -496,9 +542,10 @@ class RedisManager { if (!this.isRedisEnabled() || !this.readClient) return null; try { - const fallbackOperation = (this.hasReplicas && this.writeClient && this.isWriteHealthy) - ? () => this.writeClient!.get(key) - : undefined; + const fallbackOperation = + this.hasReplicas && this.writeClient && this.isWriteHealthy + ? () => this.writeClient!.get(key) + : undefined; return await this.executeWithRetry( () => this.readClient!.get(key), @@ -560,9 +607,10 @@ class RedisManager { if (!this.isRedisEnabled() || !this.readClient) return []; try { - const fallbackOperation = (this.hasReplicas && this.writeClient && this.isWriteHealthy) - ? () => this.writeClient!.smembers(key) - : undefined; + const fallbackOperation = + this.hasReplicas && this.writeClient && this.isWriteHealthy + ? () => this.writeClient!.smembers(key) + : undefined; return await this.executeWithRetry( () => this.readClient!.smembers(key), @@ -598,9 +646,10 @@ class RedisManager { if (!this.isRedisEnabled() || !this.readClient) return null; try { - const fallbackOperation = (this.hasReplicas && this.writeClient && this.isWriteHealthy) - ? () => this.writeClient!.hget(key, field) - : undefined; + const fallbackOperation = + this.hasReplicas && this.writeClient && this.isWriteHealthy + ? () => this.writeClient!.hget(key, field) + : undefined; return await this.executeWithRetry( () => this.readClient!.hget(key, field), @@ -632,9 +681,10 @@ class RedisManager { if (!this.isRedisEnabled() || !this.readClient) return {}; try { - const fallbackOperation = (this.hasReplicas && this.writeClient && this.isWriteHealthy) - ? () => this.writeClient!.hgetall(key) - : undefined; + const fallbackOperation = + this.hasReplicas && this.writeClient && this.isWriteHealthy + ? () => this.writeClient!.hgetall(key) + : undefined; return await this.executeWithRetry( () => this.readClient!.hgetall(key), @@ -658,18 +708,18 @@ class RedisManager { } try { - await this.executeWithRetry( - async () => { - // Add timeout to prevent hanging - return Promise.race([ - this.publisher!.publish(channel, message), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Redis publish timeout')), 3000) + await this.executeWithRetry(async () => { + // Add timeout to prevent hanging + return Promise.race([ + this.publisher!.publish(channel, message), + new Promise((_, reject) => + setTimeout( + () => reject(new Error("Redis publish timeout")), + 3000 ) - ]); - }, - "Redis PUBLISH" - ); + ) + ]); + }, "Redis PUBLISH"); return true; } catch (error) { logger.error("Redis PUBLISH error:", error); @@ -689,17 +739,20 @@ class RedisManager { if (!this.subscribers.has(channel)) { this.subscribers.set(channel, new Set()); // Only subscribe to the channel if it's the first subscriber - await this.executeWithRetry( - async () => { - return Promise.race([ - this.subscriber!.subscribe(channel), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Redis subscribe timeout')), 5000) + await this.executeWithRetry(async () => { + return Promise.race([ + this.subscriber!.subscribe(channel), + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error("Redis subscribe timeout") + ), + 5000 ) - ]); - }, - "Redis SUBSCRIBE" - ); + ) + ]); + }, "Redis SUBSCRIBE"); } this.subscribers.get(channel)!.add(callback); diff --git a/server/private/lib/redisStore.ts b/server/private/lib/redisStore.ts index 235f8f8f..2360e309 100644 --- a/server/private/lib/redisStore.ts +++ b/server/private/lib/redisStore.ts @@ -11,9 +11,9 @@ * This file is not licensed under the AGPLv3. */ -import { Store, Options, IncrementResponse } from 'express-rate-limit'; -import { rateLimitService } from './rateLimit'; -import logger from '@server/logger'; +import { Store, Options, IncrementResponse } from "express-rate-limit"; +import { rateLimitService } from "./rateLimit"; +import logger from "@server/logger"; /** * A Redis-backed rate limiting store for express-rate-limit that optimizes @@ -57,12 +57,14 @@ export default class RedisStore implements Store { * * @param options - Configuration options for the store. */ - constructor(options: { - prefix?: string; - skipFailedRequests?: boolean; - skipSuccessfulRequests?: boolean; - } = {}) { - this.prefix = options.prefix || 'express-rate-limit'; + constructor( + options: { + prefix?: string; + skipFailedRequests?: boolean; + skipSuccessfulRequests?: boolean; + } = {} + ) { + this.prefix = options.prefix || "express-rate-limit"; this.skipFailedRequests = options.skipFailedRequests || false; this.skipSuccessfulRequests = options.skipSuccessfulRequests || false; } @@ -101,7 +103,8 @@ export default class RedisStore implements Store { return { totalHits: result.totalHits || 1, - resetTime: result.resetTime || new Date(Date.now() + this.windowMs) + resetTime: + result.resetTime || new Date(Date.now() + this.windowMs) }; } catch (error) { logger.error(`RedisStore increment error for key ${key}:`, error); @@ -158,7 +161,9 @@ export default class RedisStore implements Store { */ async resetAll(): Promise { try { - logger.warn('RedisStore resetAll called - this operation can be expensive'); + logger.warn( + "RedisStore resetAll called - this operation can be expensive" + ); // Force sync all pending data first await rateLimitService.forceSyncAllPendingData(); @@ -167,9 +172,9 @@ export default class RedisStore implements Store { // scanning all Redis keys with our prefix, which could be expensive. // In production, it's better to let entries expire naturally. - logger.info('RedisStore resetAll completed (pending data synced)'); + logger.info("RedisStore resetAll completed (pending data synced)"); } catch (error) { - logger.error('RedisStore resetAll error:', error); + logger.error("RedisStore resetAll error:", error); // Don't throw - this is an optional method } } @@ -181,7 +186,9 @@ export default class RedisStore implements Store { * @param key - The identifier for a client. * @returns Current hit count and reset time, or null if no data exists. */ - async getHits(key: string): Promise<{ totalHits: number; resetTime: Date } | null> { + async getHits( + key: string + ): Promise<{ totalHits: number; resetTime: Date } | null> { try { const clientId = `${this.prefix}:${key}`; @@ -200,7 +207,8 @@ export default class RedisStore implements Store { return { totalHits: Math.max(0, (result.totalHits || 0) - 1), // Adjust for the decrement - resetTime: result.resetTime || new Date(Date.now() + this.windowMs) + resetTime: + result.resetTime || new Date(Date.now() + this.windowMs) }; } catch (error) { logger.error(`RedisStore getHits error for key ${key}:`, error); @@ -215,9 +223,9 @@ export default class RedisStore implements Store { async shutdown(): Promise { try { // The rateLimitService handles its own cleanup - logger.info('RedisStore shutdown completed'); + logger.info("RedisStore shutdown completed"); } catch (error) { - logger.error('RedisStore shutdown error:', error); + logger.error("RedisStore shutdown error:", error); } } } diff --git a/server/private/lib/resend.ts b/server/private/lib/resend.ts index 17384ea3..42a11c15 100644 --- a/server/private/lib/resend.ts +++ b/server/private/lib/resend.ts @@ -16,10 +16,10 @@ import privateConfig from "#private/lib/config"; import logger from "@server/logger"; export enum AudienceIds { - SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a", - Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20", - Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549", - Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0" + SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a", + Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20", + Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549", + Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0" } const resend = new Resend( @@ -33,7 +33,9 @@ export async function moveEmailToAudience( audienceId: AudienceIds ) { if (process.env.ENVIRONMENT !== "prod") { - logger.debug(`Skipping moving email ${email} to audience ${audienceId} in non-prod environment`); + logger.debug( + `Skipping moving email ${email} to audience ${audienceId} in non-prod environment` + ); return; } const { error, data } = await retryWithBackoff(async () => { diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 2d0e3ddb..8060ccad 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -189,7 +189,7 @@ export async function getTraefikConfig( ); if (!validation.isValid) { - logger.error( + logger.debug( `Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}` ); return; diff --git a/server/private/lib/traefik/index.ts b/server/private/lib/traefik/index.ts index 30d83181..5f2c2635 100644 --- a/server/private/lib/traefik/index.ts +++ b/server/private/lib/traefik/index.ts @@ -11,4 +11,4 @@ * This file is not licensed under the AGPLv3. */ -export * from "./getTraefikConfig"; \ No newline at end of file +export * from "./getTraefikConfig"; diff --git a/server/private/license/license.ts b/server/private/license/license.ts index 809f5ca9..db3db509 100644 --- a/server/private/license/license.ts +++ b/server/private/license/license.ts @@ -64,11 +64,14 @@ export class License { private validationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/validate`; private activationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/activate`; - private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval }); + private statusCache = new NodeCache(); private licenseKeyCache = new NodeCache(); private statusKey = "status"; private serverSecret!: string; + private phoneHomeFailureCount = 0; + private checkInProgress = false; + private doRecheck = false; private publicKey = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF @@ -83,9 +86,11 @@ LQIDAQAB constructor(private hostMeta: HostMeta) { setInterval( async () => { + this.doRecheck = true; await this.check(); + this.doRecheck = false; }, - 1000 * 60 * 60 + 1000 * this.phoneHomeInterval ); } @@ -103,6 +108,7 @@ LQIDAQAB public async forceRecheck() { this.statusCache.flushAll(); this.licenseKeyCache.flushAll(); + this.phoneHomeFailureCount = 0; return await this.check(); } @@ -118,24 +124,49 @@ LQIDAQAB } public async check(): Promise { + // If a check is already in progress, return the last known status + if (this.checkInProgress) { + logger.debug( + "License check already in progress, returning last known status" + ); + const lastStatus = this.statusCache.get(this.statusKey) as + | LicenseStatus + | undefined; + if (lastStatus) { + return lastStatus; + } + // If no cached status exists, return default status + return { + hostId: this.hostMeta.hostMetaId, + isHostLicensed: true, + isLicenseValid: false + }; + } + const status: LicenseStatus = { hostId: this.hostMeta.hostMetaId, isHostLicensed: true, isLicenseValid: false }; + this.checkInProgress = true; + try { - if (this.statusCache.has(this.statusKey)) { + if (!this.doRecheck && this.statusCache.has(this.statusKey)) { const res = this.statusCache.get("status") as LicenseStatus; return res; } - // Invalidate all - this.licenseKeyCache.flushAll(); + logger.debug("Checking license status..."); + // Build new cache in temporary Map before invalidating old cache + const newCache = new Map(); const allKeysRes = await db.select().from(licenseKey); if (allKeysRes.length === 0) { status.isHostLicensed = false; + // Invalidate all and set new cache (empty) + this.licenseKeyCache.flushAll(); + this.statusCache.set(this.statusKey, status); return status; } @@ -158,7 +189,7 @@ LQIDAQAB this.publicKey ); - this.licenseKeyCache.set(decryptedKey, { + newCache.set(decryptedKey, { licenseKey: decryptedKey, licenseKeyEncrypted: key.licenseKeyId, valid: payload.valid, @@ -177,14 +208,11 @@ LQIDAQAB ); logger.error(e); - this.licenseKeyCache.set( - key.licenseKeyId, - { - licenseKey: key.licenseKeyId, - licenseKeyEncrypted: key.licenseKeyId, - valid: false - } - ); + newCache.set(key.licenseKeyId, { + licenseKey: key.licenseKeyId, + licenseKeyEncrypted: key.licenseKeyId, + valid: false + }); } } @@ -206,17 +234,29 @@ LQIDAQAB if (!apiResponse?.success) { throw new Error(apiResponse?.error); } + // Reset failure count on success + this.phoneHomeFailureCount = 0; } catch (e) { - logger.error("Error communicating with license server:"); - logger.error(e); + this.phoneHomeFailureCount++; + if (this.phoneHomeFailureCount === 1) { + // First failure: fail silently + logger.error("Error communicating with license server:"); + logger.error(e); + logger.error(`Allowing failure. Will retry one more time at next run interval.`); + // return last known good status + return this.statusCache.get( + this.statusKey + ) as LicenseStatus; + } else { + // Subsequent failures: fail abruptly + throw e; + } } // Check and update all license keys with server response for (const key of keys) { try { - const cached = this.licenseKeyCache.get( - key.licenseKey - )!; + const cached = newCache.get(key.licenseKey)!; const licenseKeyRes = apiResponse?.data?.licenseKeys[key.licenseKey]; @@ -240,10 +280,7 @@ LQIDAQAB `Can't trust license key: ${key.licenseKey}` ); cached.valid = false; - this.licenseKeyCache.set( - key.licenseKey, - cached - ); + newCache.set(key.licenseKey, cached); continue; } @@ -274,10 +311,7 @@ LQIDAQAB }) .where(eq(licenseKey.licenseKeyId, encryptedKey)); - this.licenseKeyCache.set( - key.licenseKey, - cached - ); + newCache.set(key.licenseKey, cached); } catch (e) { logger.error(`Error validating license key: ${key}`); logger.error(e); @@ -286,9 +320,7 @@ LQIDAQAB // Compute host status for (const key of keys) { - const cached = this.licenseKeyCache.get( - key.licenseKey - )!; + const cached = newCache.get(key.licenseKey)!; if (cached.type === "host") { status.isLicenseValid = cached.valid; @@ -299,9 +331,17 @@ LQIDAQAB continue; } } + + // Invalidate old cache and set new cache + this.licenseKeyCache.flushAll(); + for (const [key, value] of newCache.entries()) { + this.licenseKeyCache.set(key, value); + } } catch (error) { logger.error("Error checking license status:"); logger.error(error); + } finally { + this.checkInProgress = false; } this.statusCache.set(this.statusKey, status); @@ -430,20 +470,58 @@ LQIDAQAB : key.instanceId })); - const response = await fetch(this.validationServerUrl, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - licenseKeys: decryptedKeys, - instanceName: this.hostMeta.hostMetaId - }) - }); + const maxAttempts = 10; + const initialRetryDelay = 1 * 1000; // 1 seconds + const exponentialFactor = 1.2; - const data = await response.json(); + let lastError: Error | undefined; - return data as ValidateLicenseAPIResponse; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await fetch(this.validationServerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + licenseKeys: decryptedKeys, + instanceName: this.hostMeta.hostMetaId + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data as ValidateLicenseAPIResponse; + } catch (error) { + lastError = + error instanceof Error ? error : new Error(String(error)); + + if (attempt < maxAttempts) { + // Calculate exponential backoff delay + const retryDelay = Math.floor( + initialRetryDelay * + Math.pow(exponentialFactor, attempt - 1) + ); + + logger.debug( + `License validation request failed (attempt ${attempt}/${maxAttempts}), retrying in ${retryDelay} ms...` + ); + await new Promise((resolve) => + setTimeout(resolve, retryDelay) + ); + } else { + logger.error( + `License validation request failed after ${maxAttempts} attempts` + ); + throw lastError; + } + } + } + + throw lastError || new Error("License validation request failed"); } } diff --git a/server/private/license/licenseJwt.ts b/server/private/license/licenseJwt.ts index f137db30..eb27b78f 100644 --- a/server/private/license/licenseJwt.ts +++ b/server/private/license/licenseJwt.ts @@ -19,10 +19,7 @@ import * as crypto from "crypto"; * @param publicKey - The public key used for verification (PEM format) * @returns The decoded payload if validation succeeds, throws an error otherwise */ -function validateJWT( - token: string, - publicKey: string -): Payload { +function validateJWT(token: string, publicKey: string): Payload { // Split the JWT into its three parts const parts = token.split("."); if (parts.length !== 3) { diff --git a/server/private/middlewares/logActionAudit.ts b/server/private/middlewares/logActionAudit.ts index 33051c3b..17cc67c0 100644 --- a/server/private/middlewares/logActionAudit.ts +++ b/server/private/middlewares/logActionAudit.ts @@ -19,6 +19,7 @@ import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; import { and, eq, lt } from "drizzle-orm"; import cache from "@server/lib/cache"; +import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; async function getActionDays(orgId: string): Promise { // check cache first @@ -40,15 +41,17 @@ async function getActionDays(orgId: string): Promise { } // store the result in cache - cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction, 300); + cache.set( + `org_${orgId}_actionDays`, + org.settingsLogRetentionDaysAction, + 300 + ); return org.settingsLogRetentionDaysAction; } export async function cleanUpOldLogs(orgId: string, retentionDays: number) { - const now = Math.floor(Date.now() / 1000); - - const cutoffTimestamp = now - retentionDays * 24 * 60 * 60; + const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); try { await db @@ -142,4 +145,3 @@ export function logActionAudit(action: ActionsEnum) { } }; } - diff --git a/server/private/middlewares/verifyCertificateAccess.ts b/server/private/middlewares/verifyCertificateAccess.ts index 1708215e..dcc57dca 100644 --- a/server/private/middlewares/verifyCertificateAccess.ts +++ b/server/private/middlewares/verifyCertificateAccess.ts @@ -28,7 +28,8 @@ export async function verifyCertificateAccess( try { // Assume user/org access is already verified const orgId = req.params.orgId; - const certId = req.params.certId || req.body?.certId || req.query?.certId; + const certId = + req.params.certId || req.body?.certId || req.query?.certId; let domainId = req.params.domainId || req.body?.domainId || req.query?.domainId; @@ -39,10 +40,12 @@ export async function verifyCertificateAccess( } if (!domainId) { - if (!certId) { return next( - createHttpError(HttpCode.BAD_REQUEST, "Must provide either certId or domainId") + createHttpError( + HttpCode.BAD_REQUEST, + "Must provide either certId or domainId" + ) ); } @@ -75,7 +78,10 @@ export async function verifyCertificateAccess( if (!domainId) { return next( - createHttpError(HttpCode.BAD_REQUEST, "Must provide either certId or domainId") + createHttpError( + HttpCode.BAD_REQUEST, + "Must provide either certId or domainId" + ) ); } diff --git a/server/private/middlewares/verifyIdpAccess.ts b/server/private/middlewares/verifyIdpAccess.ts index 87397a3d..41095684 100644 --- a/server/private/middlewares/verifyIdpAccess.ts +++ b/server/private/middlewares/verifyIdpAccess.ts @@ -24,8 +24,7 @@ export async function verifyIdpAccess( ) { try { const userId = req.user!.userId; - const idpId = - req.params.idpId || req.body.idpId || req.query.idpId; + const idpId = req.params.idpId || req.body.idpId || req.query.idpId; const orgId = req.params.orgId; if (!userId) { @@ -50,9 +49,7 @@ export async function verifyIdpAccess( .select() .from(idp) .innerJoin(idpOrg, eq(idp.idpId, idpOrg.idpId)) - .where( - and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId)) - ) + .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))) .limit(1); if (!idpRes || !idpRes.idp || !idpRes.idpOrg) { diff --git a/server/private/middlewares/verifyRemoteExitNode.ts b/server/private/middlewares/verifyRemoteExitNode.ts index 2f6d99d2..8abdc47e 100644 --- a/server/private/middlewares/verifyRemoteExitNode.ts +++ b/server/private/middlewares/verifyRemoteExitNode.ts @@ -26,7 +26,8 @@ export const verifySessionRemoteExitNodeMiddleware = async ( // get the token from the auth header const token = req.headers["authorization"]?.split(" ")[1] || ""; - const { session, remoteExitNode } = await validateRemoteExitNodeSessionToken(token); + const { session, remoteExitNode } = + await validateRemoteExitNodeSessionToken(token); if (!session || !remoteExitNode) { if (config.getRawConfig().app.log_failed_attempts) { diff --git a/server/private/routers/auditLogs/exportAccessAuditLog.ts b/server/private/routers/auditLogs/exportAccessAuditLog.ts index 89aef6cb..fbeca932 100644 --- a/server/private/routers/auditLogs/exportAccessAuditLog.ts +++ b/server/private/routers/auditLogs/exportAccessAuditLog.ts @@ -19,7 +19,11 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; -import { queryAccessAuditLogsParams, queryAccessAuditLogsQuery, queryAccess } from "./queryAccessAuditLog"; +import { + queryAccessAuditLogsParams, + queryAccessAuditLogsQuery, + queryAccess +} from "./queryAccessAuditLog"; import { generateCSV } from "@server/routers/auditLogs/generateCSV"; registry.registerPath({ @@ -67,10 +71,13 @@ export async function exportAccessAuditLogs( const log = await baseQuery.limit(data.limit).offset(data.offset); const csvData = generateCSV(log); - - res.setHeader('Content-Type', 'text/csv'); - res.setHeader('Content-Disposition', `attachment; filename="access-audit-logs-${data.orgId}-${Date.now()}.csv"`); - + + res.setHeader("Content-Type", "text/csv"); + res.setHeader( + "Content-Disposition", + `attachment; filename="access-audit-logs-${data.orgId}-${Date.now()}.csv"` + ); + return res.send(csvData); } catch (error) { logger.error(error); @@ -78,4 +85,4 @@ export async function exportAccessAuditLogs( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/private/routers/auditLogs/exportActionAuditLog.ts b/server/private/routers/auditLogs/exportActionAuditLog.ts index 12c9ff8b..1fc4d743 100644 --- a/server/private/routers/auditLogs/exportActionAuditLog.ts +++ b/server/private/routers/auditLogs/exportActionAuditLog.ts @@ -19,7 +19,11 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; -import { queryActionAuditLogsParams, queryActionAuditLogsQuery, queryAction } from "./queryActionAuditLog"; +import { + queryActionAuditLogsParams, + queryActionAuditLogsQuery, + queryAction +} from "./queryActionAuditLog"; import { generateCSV } from "@server/routers/auditLogs/generateCSV"; registry.registerPath({ @@ -60,17 +64,20 @@ export async function exportActionAuditLogs( ); } - const data = { ...parsedQuery.data, ...parsedParams.data }; + const data = { ...parsedQuery.data, ...parsedParams.data }; const baseQuery = queryAction(data); const log = await baseQuery.limit(data.limit).offset(data.offset); const csvData = generateCSV(log); - - res.setHeader('Content-Type', 'text/csv'); - res.setHeader('Content-Disposition', `attachment; filename="action-audit-logs-${data.orgId}-${Date.now()}.csv"`); - + + res.setHeader("Content-Type", "text/csv"); + res.setHeader( + "Content-Disposition", + `attachment; filename="action-audit-logs-${data.orgId}-${Date.now()}.csv"` + ); + return res.send(csvData); } catch (error) { logger.error(error); @@ -78,4 +85,4 @@ export async function exportActionAuditLogs( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/private/routers/auditLogs/index.ts b/server/private/routers/auditLogs/index.ts index ac623c4c..e1849a61 100644 --- a/server/private/routers/auditLogs/index.ts +++ b/server/private/routers/auditLogs/index.ts @@ -14,4 +14,4 @@ export * from "./queryActionAuditLog"; export * from "./exportActionAuditLog"; export * from "./queryAccessAuditLog"; -export * from "./exportAccessAuditLog"; \ No newline at end of file +export * from "./exportAccessAuditLog"; diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 769dcf55..5d3162aa 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -44,7 +44,8 @@ export const queryAccessAuditLogsQuery = z.object({ .openapi({ type: "string", format: "date-time", - description: "End time as ISO date string (defaults to current time)" + description: + "End time as ISO date string (defaults to current time)" }), action: z .union([z.boolean(), z.string()]) @@ -181,9 +182,15 @@ async function queryUniqueFilterAttributes( .where(baseConditions); return { - actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null), - resources: uniqueResources.filter((row): row is { id: number; name: string | null } => row.id !== null), - locations: uniqueLocations.map(row => row.locations).filter((location): location is string => location !== null) + actors: uniqueActors + .map((row) => row.actor) + .filter((actor): actor is string => actor !== null), + resources: uniqueResources.filter( + (row): row is { id: number; name: string | null } => row.id !== null + ), + locations: uniqueLocations + .map((row) => row.locations) + .filter((location): location is string => location !== null) }; } diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index d4a43879..eca583b4 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -44,7 +44,8 @@ export const queryActionAuditLogsQuery = z.object({ .openapi({ type: "string", format: "date-time", - description: "End time as ISO date string (defaults to current time)" + description: + "End time as ISO date string (defaults to current time)" }), action: z.string().optional(), actorType: z.string().optional(), @@ -68,8 +69,9 @@ export const queryActionAuditLogsParams = z.object({ orgId: z.string() }); -export const queryActionAuditLogsCombined = - queryActionAuditLogsQuery.merge(queryActionAuditLogsParams); +export const queryActionAuditLogsCombined = queryActionAuditLogsQuery.merge( + queryActionAuditLogsParams +); type Q = z.infer; function getWhere(data: Q) { @@ -78,7 +80,9 @@ function getWhere(data: Q) { lt(actionAuditLog.timestamp, data.timeEnd), eq(actionAuditLog.orgId, data.orgId), data.actor ? eq(actionAuditLog.actor, data.actor) : undefined, - data.actorType ? eq(actionAuditLog.actorType, data.actorType) : undefined, + data.actorType + ? eq(actionAuditLog.actorType, data.actorType) + : undefined, data.actorId ? eq(actionAuditLog.actorId, data.actorId) : undefined, data.action ? eq(actionAuditLog.action, data.action) : undefined ); @@ -135,8 +139,12 @@ async function queryUniqueFilterAttributes( .where(baseConditions); return { - actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null), - actions: uniqueActions.map(row => row.action).filter((action): action is string => action !== null), + actors: uniqueActors + .map((row) => row.actor) + .filter((actor): actor is string => actor !== null), + actions: uniqueActions + .map((row) => row.action) + .filter((action): action is string => action !== null) }; } diff --git a/server/private/routers/auth/index.ts b/server/private/routers/auth/index.ts index 39a60031..535d5887 100644 --- a/server/private/routers/auth/index.ts +++ b/server/private/routers/auth/index.ts @@ -13,4 +13,4 @@ export * from "./transferSession"; export * from "./getSessionTransferToken"; -export * from "./quickStart"; \ No newline at end of file +export * from "./quickStart"; diff --git a/server/private/routers/auth/quickStart.ts b/server/private/routers/auth/quickStart.ts index 02023a0b..612a3951 100644 --- a/server/private/routers/auth/quickStart.ts +++ b/server/private/routers/auth/quickStart.ts @@ -395,7 +395,8 @@ export async function quickStart( .values({ targetId: newTarget[0].targetId, hcEnabled: false - }).returning(); + }) + .returning(); // add the new target to the targetIps array targetIps.push(`${ip}/32`); @@ -406,7 +407,12 @@ export async function quickStart( .where(eq(newts.siteId, siteId!)) .limit(1); - await addTargets(newt.newtId, newTarget, newHealthcheck, resource.protocol); + await addTargets( + newt.newtId, + newTarget, + newHealthcheck, + resource.protocol + ); // Set resource pincode if provided if (pincode) { diff --git a/server/private/routers/billing/createCheckoutSession.ts b/server/private/routers/billing/createCheckoutSession.ts index e0e08a20..a2d8080f 100644 --- a/server/private/routers/billing/createCheckoutSession.ts +++ b/server/private/routers/billing/createCheckoutSession.ts @@ -26,8 +26,8 @@ import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing"; import { getTierPriceSet, TierId } from "@server/lib/billing/tiers"; const createCheckoutSessionSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export async function createCheckoutSession( req: Request, @@ -72,7 +72,7 @@ export async function createCheckoutSession( billing_address_collection: "required", line_items: [ { - price: standardTierPrice, // Use the standard tier + price: standardTierPrice, // Use the standard tier quantity: 1 }, ...getLineItems(getStandardFeaturePriceSet()) diff --git a/server/private/routers/billing/createPortalSession.ts b/server/private/routers/billing/createPortalSession.ts index a3a2f04f..9ebe84e0 100644 --- a/server/private/routers/billing/createPortalSession.ts +++ b/server/private/routers/billing/createPortalSession.ts @@ -24,8 +24,8 @@ import { fromError } from "zod-validation-error"; import stripe from "#private/lib/stripe"; const createPortalSessionSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export async function createPortalSession( req: Request, diff --git a/server/private/routers/billing/getOrgSubscription.ts b/server/private/routers/billing/getOrgSubscription.ts index adc4ee04..e1f8316e 100644 --- a/server/private/routers/billing/getOrgSubscription.ts +++ b/server/private/routers/billing/getOrgSubscription.ts @@ -34,8 +34,8 @@ import { } from "@server/db"; const getOrgSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); registry.registerPath({ method: "get", diff --git a/server/private/routers/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts index 9e605cca..1a343730 100644 --- a/server/private/routers/billing/getOrgUsage.ts +++ b/server/private/routers/billing/getOrgUsage.ts @@ -28,8 +28,8 @@ import { FeatureId } from "@server/lib/billing"; import { GetOrgUsageResponse } from "@server/routers/billing/types"; const getOrgSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); registry.registerPath({ method: "get", @@ -78,11 +78,23 @@ export async function getOrgUsage( // Get usage for org const usageData = []; - const siteUptime = await usageService.getUsage(orgId, FeatureId.SITE_UPTIME); + const siteUptime = await usageService.getUsage( + orgId, + FeatureId.SITE_UPTIME + ); const users = await usageService.getUsageDaily(orgId, FeatureId.USERS); - const domains = await usageService.getUsageDaily(orgId, FeatureId.DOMAINS); - const remoteExitNodes = await usageService.getUsageDaily(orgId, FeatureId.REMOTE_EXIT_NODES); - const egressData = await usageService.getUsage(orgId, FeatureId.EGRESS_DATA_MB); + const domains = await usageService.getUsageDaily( + orgId, + FeatureId.DOMAINS + ); + const remoteExitNodes = await usageService.getUsageDaily( + orgId, + FeatureId.REMOTE_EXIT_NODES + ); + const egressData = await usageService.getUsage( + orgId, + FeatureId.EGRESS_DATA_MB + ); if (siteUptime) { usageData.push(siteUptime); @@ -100,7 +112,8 @@ export async function getOrgUsage( usageData.push(remoteExitNodes); } - const orgLimits = await db.select() + const orgLimits = await db + .select() .from(limits) .where(eq(limits.orgId, orgId)); diff --git a/server/private/routers/billing/hooks/handleCustomerDeleted.ts b/server/private/routers/billing/hooks/handleCustomerDeleted.ts index aa2e6964..e4140353 100644 --- a/server/private/routers/billing/hooks/handleCustomerDeleted.ts +++ b/server/private/routers/billing/hooks/handleCustomerDeleted.ts @@ -31,9 +31,7 @@ export async function handleCustomerDeleted( return; } - await db - .delete(customers) - .where(eq(customers.customerId, customer.id)); + await db.delete(customers).where(eq(customers.customerId, customer.id)); } catch (error) { logger.error( `Error handling customer created event for ID ${customer.id}:`, diff --git a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts index 114a4b30..7a7d9149 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts @@ -12,7 +12,14 @@ */ import Stripe from "stripe"; -import { subscriptions, db, subscriptionItems, customers, userOrgs, users } from "@server/db"; +import { + subscriptions, + db, + subscriptionItems, + customers, + userOrgs, + users +} from "@server/db"; import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; @@ -43,7 +50,6 @@ export async function handleSubscriptionDeleted( .delete(subscriptionItems) .where(eq(subscriptionItems.subscriptionId, subscription.id)); - // Lookup customer to get orgId const [customer] = await db .select() @@ -58,10 +64,7 @@ export async function handleSubscriptionDeleted( return; } - await handleSubscriptionLifesycle( - customer.orgId, - subscription.status - ); + await handleSubscriptionLifesycle(customer.orgId, subscription.status); const [orgUserRes] = await db .select() diff --git a/server/private/routers/billing/index.ts b/server/private/routers/billing/index.ts index 913ae865..59fce8d6 100644 --- a/server/private/routers/billing/index.ts +++ b/server/private/routers/billing/index.ts @@ -15,4 +15,4 @@ export * from "./createCheckoutSession"; export * from "./createPortalSession"; export * from "./getOrgSubscription"; export * from "./getOrgUsage"; -export * from "./internalGetOrgTier"; \ No newline at end of file +export * from "./internalGetOrgTier"; diff --git a/server/private/routers/billing/internalGetOrgTier.ts b/server/private/routers/billing/internalGetOrgTier.ts index ec114cca..92bbc2ba 100644 --- a/server/private/routers/billing/internalGetOrgTier.ts +++ b/server/private/routers/billing/internalGetOrgTier.ts @@ -22,8 +22,8 @@ import { getOrgTierData } from "#private/lib/billing"; import { GetOrgTierResponse } from "@server/routers/billing/types"; const getOrgSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export async function getOrgTier( req: Request, diff --git a/server/private/routers/billing/subscriptionLifecycle.ts b/server/private/routers/billing/subscriptionLifecycle.ts index 06b2a2a8..0fc75835 100644 --- a/server/private/routers/billing/subscriptionLifecycle.ts +++ b/server/private/routers/billing/subscriptionLifecycle.ts @@ -11,11 +11,18 @@ * This file is not licensed under the AGPLv3. */ -import { freeLimitSet, limitsService, subscribedLimitSet } from "@server/lib/billing"; +import { + freeLimitSet, + limitsService, + subscribedLimitSet +} from "@server/lib/billing"; import { usageService } from "@server/lib/billing/usageService"; import logger from "@server/logger"; -export async function handleSubscriptionLifesycle(orgId: string, status: string) { +export async function handleSubscriptionLifesycle( + orgId: string, + status: string +) { switch (status) { case "active": await limitsService.applyLimitSetToOrg(orgId, subscribedLimitSet); @@ -42,4 +49,4 @@ export async function handleSubscriptionLifesycle(orgId: string, status: string) default: break; } -} \ No newline at end of file +} diff --git a/server/private/routers/billing/webhooks.ts b/server/private/routers/billing/webhooks.ts index 24ad1074..9c64350c 100644 --- a/server/private/routers/billing/webhooks.ts +++ b/server/private/routers/billing/webhooks.ts @@ -32,12 +32,13 @@ export async function billingWebhookHandler( next: NextFunction ): Promise { let event: Stripe.Event = req.body; - const endpointSecret = privateConfig.getRawPrivateConfig().stripe?.webhook_secret; + const endpointSecret = + privateConfig.getRawPrivateConfig().stripe?.webhook_secret; if (!endpointSecret) { - logger.warn("Stripe webhook secret is not configured. Webhook events will not be priocessed."); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "") + logger.warn( + "Stripe webhook secret is not configured. Webhook events will not be priocessed." ); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "")); } // Only verify the event if you have an endpoint secret defined. @@ -49,7 +50,10 @@ export async function billingWebhookHandler( if (!signature) { logger.info("No stripe signature found in headers."); return next( - createHttpError(HttpCode.BAD_REQUEST, "No stripe signature found in headers") + createHttpError( + HttpCode.BAD_REQUEST, + "No stripe signature found in headers" + ) ); } @@ -62,7 +66,10 @@ export async function billingWebhookHandler( } catch (err) { logger.error(`Webhook signature verification failed.`, err); return next( - createHttpError(HttpCode.UNAUTHORIZED, "Webhook signature verification failed") + createHttpError( + HttpCode.UNAUTHORIZED, + "Webhook signature verification failed" + ) ); } } diff --git a/server/private/routers/certificates/getCertificate.ts b/server/private/routers/certificates/getCertificate.ts index 4ff8184e..d06a1bad 100644 --- a/server/private/routers/certificates/getCertificate.ts +++ b/server/private/routers/certificates/getCertificate.ts @@ -24,10 +24,10 @@ import { registry } from "@server/openApi"; import { GetCertificateResponse } from "@server/routers/certificates/types"; const getCertificateSchema = z.strictObject({ - domainId: z.string(), - domain: z.string().min(1).max(255), - orgId: z.string() - }); + domainId: z.string(), + domain: z.string().min(1).max(255), + orgId: z.string() +}); async function query(domainId: string, domain: string) { const [domainRecord] = await db @@ -42,8 +42,8 @@ async function query(domainId: string, domain: string) { let existing: any[] = []; if (domainRecord.type == "ns") { - const domainLevelDown = domain.split('.').slice(1).join('.'); - + const domainLevelDown = domain.split(".").slice(1).join("."); + existing = await db .select({ certId: certificates.certId, @@ -64,7 +64,7 @@ async function query(domainId: string, domain: string) { eq(certificates.wildcard, true), // only NS domains can have wildcard certs or( eq(certificates.domain, domain), - eq(certificates.domain, domainLevelDown), + eq(certificates.domain, domainLevelDown) ) ) ); @@ -102,8 +102,7 @@ registry.registerPath({ tags: ["Certificate"], request: { params: z.object({ - domainId: z - .string(), + domainId: z.string(), domain: z.string().min(1).max(255), orgId: z.string() }) @@ -133,7 +132,9 @@ export async function getCertificate( if (!cert) { logger.warn(`Certificate not found for domain: ${domainId}`); - return next(createHttpError(HttpCode.NOT_FOUND, "Certificate not found")); + return next( + createHttpError(HttpCode.NOT_FOUND, "Certificate not found") + ); } return response(res, { diff --git a/server/private/routers/certificates/index.ts b/server/private/routers/certificates/index.ts index e1b81ae1..b1543e5d 100644 --- a/server/private/routers/certificates/index.ts +++ b/server/private/routers/certificates/index.ts @@ -12,4 +12,4 @@ */ export * from "./getCertificate"; -export * from "./restartCertificate"; \ No newline at end of file +export * from "./restartCertificate"; diff --git a/server/private/routers/certificates/restartCertificate.ts b/server/private/routers/certificates/restartCertificate.ts index a6ee5460..0e4b1910 100644 --- a/server/private/routers/certificates/restartCertificate.ts +++ b/server/private/routers/certificates/restartCertificate.ts @@ -25,9 +25,9 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const restartCertificateParamsSchema = z.strictObject({ - certId: z.string().transform(stoi).pipe(z.int().positive()), - orgId: z.string() - }); + certId: z.string().transform(stoi).pipe(z.int().positive()), + orgId: z.string() +}); registry.registerPath({ method: "post", @@ -36,10 +36,7 @@ registry.registerPath({ tags: ["Certificate"], request: { params: z.object({ - certId: z - .string() - .transform(stoi) - .pipe(z.int().positive()), + certId: z.string().transform(stoi).pipe(z.int().positive()), orgId: z.string() }) }, @@ -94,7 +91,7 @@ export async function restartCertificate( .set({ status: "pending", errorMessage: null, - lastRenewalAttempt: Math.floor(Date.now() / 1000) + lastRenewalAttempt: Math.floor(Date.now() / 1000) }) .where(eq(certificates.certId, certId)); diff --git a/server/private/routers/domain/checkDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts index 6c9cb23c..db9a4b46 100644 --- a/server/private/routers/domain/checkDomainNamespaceAvailability.ts +++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts @@ -26,8 +26,8 @@ import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; const paramsSchema = z.strictObject({}); const querySchema = z.strictObject({ - subdomain: z.string() - }); + subdomain: z.string() +}); registry.registerPath({ method: "get", diff --git a/server/private/routers/domain/index.ts b/server/private/routers/domain/index.ts index da9cec3f..3f4bbbf2 100644 --- a/server/private/routers/domain/index.ts +++ b/server/private/routers/domain/index.ts @@ -12,4 +12,4 @@ */ export * from "./checkDomainNamespaceAvailability"; -export * from "./listDomainNamespaces"; \ No newline at end of file +export * from "./listDomainNamespaces"; diff --git a/server/private/routers/domain/listDomainNamespaces.ts b/server/private/routers/domain/listDomainNamespaces.ts index 29d5d201..180613a8 100644 --- a/server/private/routers/domain/listDomainNamespaces.ts +++ b/server/private/routers/domain/listDomainNamespaces.ts @@ -26,19 +26,19 @@ import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.strictObject({}); const querySchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function query(limit: number, offset: number) { const res = await db diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 3e297df5..568f2b35 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -30,7 +30,7 @@ import { verifyUserHasAction, verifyUserIsServerAdmin, verifySiteAccess, - verifyClientAccess, + verifyClientAccess } from "@server/middlewares"; import { ActionsEnum } from "@server/auth/actions"; import { @@ -409,6 +409,8 @@ authenticated.get( authenticated.post( "/re-key/:clientId/regenerate-client-secret", + verifyValidLicense, + verifyValidSubscription, verifyClientAccess, verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateClientSecret @@ -416,15 +418,18 @@ authenticated.post( authenticated.post( "/re-key/:siteId/regenerate-site-secret", + verifyValidLicense, + verifyValidSubscription, verifySiteAccess, verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateSiteSecret ); authenticated.put( - "/re-key/:orgId/reGenerate-remote-exit-node-secret", + "/re-key/:orgId/regenerate-remote-exit-node-secret", verifyValidLicense, + verifyValidSubscription, verifyOrgAccess, - verifyUserHasAction(ActionsEnum.updateRemoteExitNode), + verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateExitNodeSecret ); diff --git a/server/private/routers/gerbil/receiveBandwidth.ts b/server/private/routers/gerbil/receiveBandwidth.ts deleted file mode 100644 index de0b2d2b..00000000 --- a/server/private/routers/gerbil/receiveBandwidth.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index a61f37b2..3accc500 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -79,86 +79,72 @@ import semver from "semver"; // Zod schemas for request validation const getResourceByDomainParamsSchema = z.strictObject({ - domain: z.string().min(1, "Domain is required") - }); + domain: z.string().min(1, "Domain is required") +}); const getUserSessionParamsSchema = z.strictObject({ - userSessionId: z.string().min(1, "User session ID is required") - }); + userSessionId: z.string().min(1, "User session ID is required") +}); const getUserOrgRoleParamsSchema = z.strictObject({ - userId: z.string().min(1, "User ID is required"), - orgId: z.string().min(1, "Organization ID is required") - }); + userId: z.string().min(1, "User ID is required"), + orgId: z.string().min(1, "Organization ID is required") +}); const getRoleResourceAccessParamsSchema = z.strictObject({ - roleId: z - .string() - .transform(Number) - .pipe( - z.int().positive("Role ID must be a positive integer") - ), - resourceId: z - .string() - .transform(Number) - .pipe( - z.int() - .positive("Resource ID must be a positive integer") - ) - }); + roleId: z + .string() + .transform(Number) + .pipe(z.int().positive("Role ID must be a positive integer")), + resourceId: z + .string() + .transform(Number) + .pipe(z.int().positive("Resource ID must be a positive integer")) +}); const getUserResourceAccessParamsSchema = z.strictObject({ - userId: z.string().min(1, "User ID is required"), - resourceId: z - .string() - .transform(Number) - .pipe( - z.int() - .positive("Resource ID must be a positive integer") - ) - }); + userId: z.string().min(1, "User ID is required"), + resourceId: z + .string() + .transform(Number) + .pipe(z.int().positive("Resource ID must be a positive integer")) +}); const getResourceRulesParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe( - z.int() - .positive("Resource ID must be a positive integer") - ) - }); + resourceId: z + .string() + .transform(Number) + .pipe(z.int().positive("Resource ID must be a positive integer")) +}); const validateResourceSessionTokenParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe( - z.int() - .positive("Resource ID must be a positive integer") - ) - }); + resourceId: z + .string() + .transform(Number) + .pipe(z.int().positive("Resource ID must be a positive integer")) +}); const validateResourceSessionTokenBodySchema = z.strictObject({ - token: z.string().min(1, "Token is required") - }); + token: z.string().min(1, "Token is required") +}); const validateResourceAccessTokenBodySchema = z.strictObject({ - accessTokenId: z.string().optional(), - resourceId: z.number().optional(), - accessToken: z.string() - }); + accessTokenId: z.string().optional(), + resourceId: z.number().optional(), + accessToken: z.string() +}); // Certificates by domains query validation const getCertificatesByDomainsQuerySchema = z.strictObject({ - // Accept domains as string or array (domains or domains[]) - domains: z - .union([z.array(z.string().min(1)), z.string().min(1)]) - .optional(), - // Handle array format from query parameters (domains[]) - "domains[]": z - .union([z.array(z.string().min(1)), z.string().min(1)]) - .optional() - }); + // Accept domains as string or array (domains or domains[]) + domains: z + .union([z.array(z.string().min(1)), z.string().min(1)]) + .optional(), + // Handle array format from query parameters (domains[]) + "domains[]": z + .union([z.array(z.string().min(1)), z.string().min(1)]) + .optional() +}); // Type exports for request schemas export type GetResourceByDomainParams = z.infer< @@ -566,8 +552,8 @@ hybridRouter.get( ); const getOrgLoginPageParamsSchema = z.strictObject({ - orgId: z.string().min(1) - }); + orgId: z.string().min(1) +}); hybridRouter.get( "/org/:orgId/login-page", @@ -1408,8 +1394,16 @@ hybridRouter.post( ); } - const { olmId, newtId, ip, port, timestamp, token, publicKey, reachableAt } = - parsedParams.data; + const { + olmId, + newtId, + ip, + port, + timestamp, + token, + publicKey, + reachableAt + } = parsedParams.data; const destinations = await updateAndGenerateEndpointDestinations( olmId, diff --git a/server/private/routers/integration.ts b/server/private/routers/integration.ts index 7ce378d1..9eefff8f 100644 --- a/server/private/routers/integration.ts +++ b/server/private/routers/integration.ts @@ -18,7 +18,7 @@ import * as logs from "#private/routers/auditLogs"; import { verifyApiKeyHasAction, verifyApiKeyIsRoot, - verifyApiKeyOrgAccess, + verifyApiKeyOrgAccess } from "@server/middlewares"; import { verifyValidSubscription, @@ -26,7 +26,10 @@ import { } from "#private/middlewares"; import { ActionsEnum } from "@server/auth/actions"; -import { unauthenticated as ua, authenticated as a } from "@server/routers/integration"; +import { + unauthenticated as ua, + authenticated as a +} from "@server/routers/integration"; import { logActionAudit } from "#private/middlewares"; export const unauthenticated = ua; @@ -37,7 +40,7 @@ authenticated.post( verifyApiKeyIsRoot, // We are the only ones who can use root key so its fine verifyApiKeyHasAction(ActionsEnum.sendUsageNotification), logActionAudit(ActionsEnum.sendUsageNotification), - org.sendUsageNotification, + org.sendUsageNotification ); authenticated.delete( @@ -45,7 +48,7 @@ authenticated.delete( verifyApiKeyIsRoot, verifyApiKeyHasAction(ActionsEnum.deleteIdp), logActionAudit(ActionsEnum.deleteIdp), - orgIdp.deleteOrgIdp, + orgIdp.deleteOrgIdp ); authenticated.get( diff --git a/server/private/routers/license/activateLicense.ts b/server/private/routers/license/activateLicense.ts index 55b7827e..f6c8d266 100644 --- a/server/private/routers/license/activateLicense.ts +++ b/server/private/routers/license/activateLicense.ts @@ -21,8 +21,8 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; const bodySchema = z.strictObject({ - licenseKey: z.string().min(1).max(255) - }); + licenseKey: z.string().min(1).max(255) +}); export async function activateLicense( req: Request, diff --git a/server/private/routers/license/deleteLicenseKey.ts b/server/private/routers/license/deleteLicenseKey.ts index 6f5469fc..80212e6a 100644 --- a/server/private/routers/license/deleteLicenseKey.ts +++ b/server/private/routers/license/deleteLicenseKey.ts @@ -24,8 +24,8 @@ import { licenseKey } from "@server/db"; import license from "#private/license/license"; const paramsSchema = z.strictObject({ - licenseKey: z.string().min(1).max(255) - }); + licenseKey: z.string().min(1).max(255) +}); export async function deleteLicenseKey( req: Request, diff --git a/server/private/routers/loginPage/createLoginPage.ts b/server/private/routers/loginPage/createLoginPage.ts index 75744026..b5e8ccff 100644 --- a/server/private/routers/loginPage/createLoginPage.ts +++ b/server/private/routers/loginPage/createLoginPage.ts @@ -36,13 +36,13 @@ import { build } from "@server/build"; import { CreateLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const bodySchema = z.strictObject({ - subdomain: z.string().nullable().optional(), - domainId: z.string() - }); + subdomain: z.string().nullable().optional(), + domainId: z.string() +}); export type CreateLoginPageBody = z.infer; @@ -149,12 +149,20 @@ export async function createLoginPage( let returned: LoginPage | undefined; await db.transaction(async (trx) => { - const orgSites = await trx .select() .from(sites) - .innerJoin(exitNodes, eq(exitNodes.exitNodeId, sites.exitNodeId)) - .where(and(eq(sites.orgId, orgId), eq(exitNodes.type, "gerbil"), eq(exitNodes.online, true))) + .innerJoin( + exitNodes, + eq(exitNodes.exitNodeId, sites.exitNodeId) + ) + .where( + and( + eq(sites.orgId, orgId), + eq(exitNodes.type, "gerbil"), + eq(exitNodes.online, true) + ) + ) .limit(10); let exitNodesList = orgSites.map((s) => s.exitNodes); @@ -163,7 +171,12 @@ export async function createLoginPage( exitNodesList = await trx .select() .from(exitNodes) - .where(and(eq(exitNodes.type, "gerbil"), eq(exitNodes.online, true))) + .where( + and( + eq(exitNodes.type, "gerbil"), + eq(exitNodes.online, true) + ) + ) .limit(10); } diff --git a/server/private/routers/loginPage/deleteLoginPage.ts b/server/private/routers/loginPage/deleteLoginPage.ts index 5271ebd8..0d17a731 100644 --- a/server/private/routers/loginPage/deleteLoginPage.ts +++ b/server/private/routers/loginPage/deleteLoginPage.ts @@ -78,15 +78,11 @@ export async function deleteLoginPage( // if (!leftoverLinks.length) { await db .delete(loginPage) - .where( - eq(loginPage.loginPageId, parsedParams.data.loginPageId) - ); + .where(eq(loginPage.loginPageId, parsedParams.data.loginPageId)); await db .delete(loginPageOrg) - .where( - eq(loginPageOrg.loginPageId, parsedParams.data.loginPageId) - ); + .where(eq(loginPageOrg.loginPageId, parsedParams.data.loginPageId)); // } return response(res, { diff --git a/server/private/routers/loginPage/getLoginPage.ts b/server/private/routers/loginPage/getLoginPage.ts index b3bde203..73f6a357 100644 --- a/server/private/routers/loginPage/getLoginPage.ts +++ b/server/private/routers/loginPage/getLoginPage.ts @@ -23,8 +23,8 @@ import { fromError } from "zod-validation-error"; import { GetLoginPageResponse } from "@server/routers/loginPage/types"; const paramsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); async function query(orgId: string) { const [res] = await db diff --git a/server/private/routers/loginPage/updateLoginPage.ts b/server/private/routers/loginPage/updateLoginPage.ts index 0d02b124..bda614d3 100644 --- a/server/private/routers/loginPage/updateLoginPage.ts +++ b/server/private/routers/loginPage/updateLoginPage.ts @@ -35,7 +35,8 @@ const paramsSchema = z }) .strict(); -const bodySchema = z.strictObject({ +const bodySchema = z + .strictObject({ subdomain: subdomainSchema.nullable().optional(), domainId: z.string().optional() }) @@ -86,7 +87,7 @@ export async function updateLoginPage( const { loginPageId, orgId } = parsedParams.data; - if (build === "saas"){ + if (build === "saas") { const { tier } = await getOrgTierData(orgId); const subscribed = tier === TierId.STANDARD; if (!subscribed) { @@ -182,7 +183,10 @@ export async function updateLoginPage( } // update the full domain if it has changed - if (fullDomain && fullDomain !== existingLoginPage?.fullDomain) { + if ( + fullDomain && + fullDomain !== existingLoginPage?.fullDomain + ) { await db .update(loginPage) .set({ fullDomain }) diff --git a/server/private/routers/misc/sendSupportEmail.ts b/server/private/routers/misc/sendSupportEmail.ts index f1f7a919..404a2501 100644 --- a/server/private/routers/misc/sendSupportEmail.ts +++ b/server/private/routers/misc/sendSupportEmail.ts @@ -23,9 +23,9 @@ import SupportEmail from "@server/emails/templates/SupportEmail"; import config from "@server/lib/config"; const bodySchema = z.strictObject({ - body: z.string().min(1), - subject: z.string().min(1).max(255) - }); + body: z.string().min(1), + subject: z.string().min(1).max(255) +}); export async function sendSupportEmail( req: Request, diff --git a/server/private/routers/org/index.ts b/server/private/routers/org/index.ts index 189c5323..8d11c42d 100644 --- a/server/private/routers/org/index.ts +++ b/server/private/routers/org/index.ts @@ -11,4 +11,4 @@ * This file is not licensed under the AGPLv3. */ -export * from "./sendUsageNotifications"; \ No newline at end of file +export * from "./sendUsageNotifications"; diff --git a/server/private/routers/org/sendUsageNotifications.ts b/server/private/routers/org/sendUsageNotifications.ts index 3ef27f91..4aa42152 100644 --- a/server/private/routers/org/sendUsageNotifications.ts +++ b/server/private/routers/org/sendUsageNotifications.ts @@ -35,10 +35,12 @@ const sendUsageNotificationBodySchema = z.object({ notificationType: z.enum(["approaching_70", "approaching_90", "reached"]), limitName: z.string(), currentUsage: z.number(), - usageLimit: z.number(), + usageLimit: z.number() }); -type SendUsageNotificationRequest = z.infer; +type SendUsageNotificationRequest = z.infer< + typeof sendUsageNotificationBodySchema +>; export type SendUsageNotificationResponse = { success: boolean; @@ -97,17 +99,13 @@ async function getOrgAdmins(orgId: string) { .where( and( eq(userOrgs.orgId, orgId), - or( - eq(userOrgs.isOwner, true), - eq(roles.isAdmin, true) - ) + or(eq(userOrgs.isOwner, true), eq(roles.isAdmin, true)) ) ); // Filter to only include users with verified emails - const orgAdmins = admins.filter(admin => - admin.email && - admin.email.length > 0 + const orgAdmins = admins.filter( + (admin) => admin.email && admin.email.length > 0 ); return orgAdmins; @@ -119,7 +117,9 @@ export async function sendUsageNotification( next: NextFunction ): Promise { try { - const parsedParams = sendUsageNotificationParamsSchema.safeParse(req.params); + const parsedParams = sendUsageNotificationParamsSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -140,12 +140,8 @@ export async function sendUsageNotification( } const { orgId } = parsedParams.data; - const { - notificationType, - limitName, - currentUsage, - usageLimit, - } = parsedBody.data; + const { notificationType, limitName, currentUsage, usageLimit } = + parsedBody.data; // Verify organization exists const org = await db @@ -192,7 +188,10 @@ export async function sendUsageNotification( let template; let subject; - if (notificationType === "approaching_70" || notificationType === "approaching_90") { + if ( + notificationType === "approaching_70" || + notificationType === "approaching_90" + ) { template = NotifyUsageLimitApproaching({ email: admin.email, limitName, @@ -220,10 +219,15 @@ export async function sendUsageNotification( emailsSent++; adminEmails.push(admin.email); - - logger.info(`Usage notification sent to admin ${admin.email} for org ${orgId}`); + + logger.info( + `Usage notification sent to admin ${admin.email} for org ${orgId}` + ); } catch (emailError) { - logger.error(`Failed to send usage notification to ${admin.email}:`, emailError); + logger.error( + `Failed to send usage notification to ${admin.email}:`, + emailError + ); // Continue with other admins even if one fails } } @@ -239,11 +243,13 @@ export async function sendUsageNotification( message: `Usage notifications sent to ${emailsSent} administrators`, status: HttpCode.OK }); - } catch (error) { logger.error("Error sending usage notifications:", error); return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to send usage notifications") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to send usage notifications" + ) ); } } diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts index c3ce774e..709f6167 100644 --- a/server/private/routers/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -32,19 +32,19 @@ import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); const bodySchema = z.strictObject({ - name: z.string().nonempty(), - clientId: z.string().nonempty(), - clientSecret: z.string().nonempty(), - authUrl: z.url(), - tokenUrl: z.url(), - identifierPath: z.string().nonempty(), - emailPath: z.string().optional(), - namePath: z.string().optional(), - scopes: z.string().nonempty(), - autoProvision: z.boolean().optional(), - variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"), - roleMapping: z.string().optional() - }); + name: z.string().nonempty(), + clientId: z.string().nonempty(), + clientSecret: z.string().nonempty(), + authUrl: z.url(), + tokenUrl: z.url(), + identifierPath: z.string().nonempty(), + emailPath: z.string().optional(), + namePath: z.string().optional(), + scopes: z.string().nonempty(), + autoProvision: z.boolean().optional(), + variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"), + roleMapping: z.string().optional() +}); // registry.registerPath({ // method: "put", @@ -158,7 +158,10 @@ export async function createOrgOidcIdp( }); }); - const redirectUrl = await generateOidcRedirectUrl(idpId as number, orgId); + const redirectUrl = await generateOidcRedirectUrl( + idpId as number, + orgId + ); return response(res, { data: { diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts index ca0112b2..721b91cb 100644 --- a/server/private/routers/orgIdp/deleteOrgIdp.ts +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -66,12 +66,7 @@ export async function deleteOrgIdp( .where(eq(idp.idpId, idpId)); if (!existingIdp) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "IdP not found" - ) - ); + return next(createHttpError(HttpCode.NOT_FOUND, "IdP not found")); } // Delete the IDP and its related records in a transaction @@ -82,14 +77,10 @@ export async function deleteOrgIdp( .where(eq(idpOidcConfig.idpId, idpId)); // Delete IDP-org mappings - await trx - .delete(idpOrg) - .where(eq(idpOrg.idpId, idpId)); + await trx.delete(idpOrg).where(eq(idpOrg.idpId, idpId)); // Delete the IDP itself - await trx - .delete(idp) - .where(eq(idp.idpId, idpId)); + await trx.delete(idp).where(eq(idp.idpId, idpId)); }); return response(res, { diff --git a/server/private/routers/orgIdp/getOrgIdp.ts b/server/private/routers/orgIdp/getOrgIdp.ts index 3ba85412..01ddc0f7 100644 --- a/server/private/routers/orgIdp/getOrgIdp.ts +++ b/server/private/routers/orgIdp/getOrgIdp.ts @@ -93,7 +93,10 @@ export async function getOrgIdp( idpRes.idpOidcConfig!.clientId = decrypt(clientId, key); } - const redirectUrl = await generateOidcRedirectUrl(idpRes.idp.idpId, orgId); + const redirectUrl = await generateOidcRedirectUrl( + idpRes.idp.idpId, + orgId + ); return response(res, { data: { diff --git a/server/private/routers/orgIdp/index.ts b/server/private/routers/orgIdp/index.ts index 562582c6..9cf937a4 100644 --- a/server/private/routers/orgIdp/index.ts +++ b/server/private/routers/orgIdp/index.ts @@ -15,4 +15,4 @@ export * from "./createOrgOidcIdp"; export * from "./getOrgIdp"; export * from "./listOrgIdps"; export * from "./updateOrgOidcIdp"; -export * from "./deleteOrgIdp"; \ No newline at end of file +export * from "./deleteOrgIdp"; diff --git a/server/private/routers/orgIdp/listOrgIdps.ts b/server/private/routers/orgIdp/listOrgIdps.ts index 646d808c..36cbc627 100644 --- a/server/private/routers/orgIdp/listOrgIdps.ts +++ b/server/private/routers/orgIdp/listOrgIdps.ts @@ -25,23 +25,23 @@ import { OpenAPITags, registry } from "@server/openApi"; import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; const querySchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); const paramsSchema = z.strictObject({ - orgId: z.string().nonempty() - }); + orgId: z.string().nonempty() +}); async function query(orgId: string, limit: number, offset: number) { const res = await db diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts index 3826f6b3..f29e4fc2 100644 --- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts @@ -36,18 +36,18 @@ const paramsSchema = z .strict(); const bodySchema = z.strictObject({ - name: z.string().optional(), - clientId: z.string().optional(), - clientSecret: z.string().optional(), - authUrl: z.string().optional(), - tokenUrl: z.string().optional(), - identifierPath: z.string().optional(), - emailPath: z.string().optional(), - namePath: z.string().optional(), - scopes: z.string().optional(), - autoProvision: z.boolean().optional(), - roleMapping: z.string().optional() - }); + name: z.string().optional(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), + authUrl: z.string().optional(), + tokenUrl: z.string().optional(), + identifierPath: z.string().optional(), + emailPath: z.string().optional(), + namePath: z.string().optional(), + scopes: z.string().optional(), + autoProvision: z.boolean().optional(), + roleMapping: z.string().optional() +}); export type UpdateOrgIdpResponse = { idpId: number; diff --git a/server/private/routers/re-key/index.ts b/server/private/routers/re-key/index.ts index 41a1c967..9c1bccf8 100644 --- a/server/private/routers/re-key/index.ts +++ b/server/private/routers/re-key/index.ts @@ -13,4 +13,4 @@ export * from "./reGenerateClientSecret"; export * from "./reGenerateSiteSecret"; -export * from "./reGenerateExitNodeSecret"; \ No newline at end of file +export * from "./reGenerateExitNodeSecret"; diff --git a/server/private/routers/re-key/reGenerateClientSecret.ts b/server/private/routers/re-key/reGenerateClientSecret.ts index 85b3f4a6..5478c690 100644 --- a/server/private/routers/re-key/reGenerateClientSecret.ts +++ b/server/private/routers/re-key/reGenerateClientSecret.ts @@ -13,7 +13,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, olms, } from "@server/db"; +import { db, Olm, olms } from "@server/db"; import { clients } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -23,37 +23,19 @@ import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { hashPassword } from "@server/auth/password"; +import { disconnectClient, sendToClient } from "#private/routers/ws"; const reGenerateSecretParamsSchema = z.strictObject({ - clientId: z.string().transform(Number).pipe(z.int().positive()) - }); - -const reGenerateSecretBodySchema = z.strictObject({ - olmId: z.string().min(1).optional(), - secret: z.string().min(1).optional(), - - }); - -export type ReGenerateSecretBody = z.infer; - -registry.registerPath({ - method: "post", - path: "/re-key/{clientId}/regenerate-client-secret", - description: "Regenerate a client's OLM credentials by its client ID.", - tags: [OpenAPITags.Client], - request: { - params: reGenerateSecretParamsSchema, - body: { - content: { - "application/json": { - schema: reGenerateSecretBodySchema - } - } - } - }, - responses: {} + clientId: z.string().transform(Number).pipe(z.int().positive()) }); +const reGenerateSecretBodySchema = z.strictObject({ + // olmId: z.string().min(1).optional(), + secret: z.string().min(1), + disconnect: z.boolean().optional().default(true) +}); + +export type ReGenerateSecretBody = z.infer; export async function reGenerateClientSecret( req: Request, @@ -71,7 +53,7 @@ export async function reGenerateClientSecret( ); } - const { olmId, secret } = parsedBody.data; + const { secret, disconnect } = parsedBody.data; const parsedParams = reGenerateSecretParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -85,11 +67,7 @@ export async function reGenerateClientSecret( const { clientId } = parsedParams.data; - let secretHash = undefined; - if (secret) { - secretHash = await hashPassword(secret); - } - + const secretHash = await hashPassword(secret); // Fetch the client to make sure it exists and the user has access to it const [client] = await db @@ -107,24 +85,59 @@ export async function reGenerateClientSecret( ); } - const [existingOlm] = await db + const existingOlms = await db .select() .from(olms) - .where(eq(olms.clientId, clientId)) - .limit(1); + .where(eq(olms.clientId, clientId)); - if (existingOlm && olmId && secretHash) { - await db - .update(olms) - .set({ - olmId, - secretHash - }) - .where(eq(olms.clientId, clientId)); + if (existingOlms.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `No OLM found for client ID ${clientId}` + ) + ); + } + + if (existingOlms.length > 1) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Multiple OLM entries found for client ID ${clientId}` + ) + ); + } + + await db + .update(olms) + .set({ + secretHash + }) + .where(eq(olms.olmId, existingOlms[0].olmId)); + + // Only disconnect if explicitly requested + if (disconnect) { + const payload = { + type: `olm/terminate`, + data: {} + }; + // Don't await this to prevent blocking the response + sendToClient(existingOlms[0].olmId, payload).catch((error) => { + logger.error( + "Failed to send termination message to olm:", + error + ); + }); + + disconnectClient(existingOlms[0].olmId).catch((error) => { + logger.error("Failed to disconnect olm after re-key:", error); + }); } return response(res, { - data: existingOlm, + data: { + olmId: existingOlms[0].olmId + }, success: true, error: false, message: "Credentials regenerated successfully", diff --git a/server/private/routers/re-key/reGenerateExitNodeSecret.ts b/server/private/routers/re-key/reGenerateExitNodeSecret.ts index ee3a7a87..021d2ce9 100644 --- a/server/private/routers/re-key/reGenerateExitNodeSecret.ts +++ b/server/private/routers/re-key/reGenerateExitNodeSecret.ts @@ -12,7 +12,14 @@ */ import { NextFunction, Request, Response } from "express"; -import { db, exitNodes, exitNodeOrgs, ExitNode, ExitNodeOrg } from "@server/db"; +import { + db, + exitNodes, + exitNodeOrgs, + ExitNode, + ExitNodeOrg, + RemoteExitNode +} from "@server/db"; import HttpCode from "@server/types/HttpCode"; import { z } from "zod"; import { remoteExitNodes } from "@server/db"; @@ -22,35 +29,17 @@ import { fromError } from "zod-validation-error"; import { hashPassword } from "@server/auth/password"; import logger from "@server/logger"; import { and, eq } from "drizzle-orm"; -import { UpdateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; import { OpenAPITags, registry } from "@server/openApi"; +import { disconnectClient, sendToClient } from "#private/routers/ws"; export const paramsSchema = z.object({ orgId: z.string() }); const bodySchema = z.strictObject({ - remoteExitNodeId: z.string().length(15), - secret: z.string().length(48) - }); - - -registry.registerPath({ - method: "post", - path: "/re-key/{orgId}/regenerate-secret", - description: "Regenerate a exit node credentials by its org ID.", - tags: [OpenAPITags.Org], - request: { - params: paramsSchema, - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } - }, - responses: {} + remoteExitNodeId: z.string().length(15), + secret: z.string().length(48), + disconnect: z.boolean().optional().default(true) }); export async function reGenerateExitNodeSecret( @@ -79,13 +68,7 @@ export async function reGenerateExitNodeSecret( ); } - const { remoteExitNodeId, secret } = parsedBody.data; - - if (req.user && !req.userOrgRoleId) { - return next( - createHttpError(HttpCode.FORBIDDEN, "User does not have a role") - ); - } + const { remoteExitNodeId, secret, disconnect } = parsedBody.data; const [existingRemoteExitNode] = await db .select() @@ -94,7 +77,10 @@ export async function reGenerateExitNodeSecret( if (!existingRemoteExitNode) { return next( - createHttpError(HttpCode.NOT_FOUND, "Remote Exit Node does not exist") + createHttpError( + HttpCode.NOT_FOUND, + "Remote Exit Node does not exist" + ) ); } @@ -105,15 +91,39 @@ export async function reGenerateExitNodeSecret( .set({ secretHash }) .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)); - return response(res, { - data: { - remoteExitNodeId, - secret, - }, + // Only disconnect if explicitly requested + if (disconnect) { + const payload = { + type: `remoteExitNode/terminate`, + data: {} + }; + // Don't await this to prevent blocking the response + sendToClient( + existingRemoteExitNode.remoteExitNodeId, + payload + ).catch((error) => { + logger.error( + "Failed to send termination message to remote exit node:", + error + ); + }); + + disconnectClient(existingRemoteExitNode.remoteExitNodeId).catch( + (error) => { + logger.error( + "Failed to disconnect remote exit node after re-key:", + error + ); + } + ); + } + + return response(res, { + data: null, success: true, error: false, message: "Remote Exit Node secret updated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (e) { logger.error("Failed to update remoteExitNode", e); diff --git a/server/private/routers/re-key/reGenerateSiteSecret.ts b/server/private/routers/re-key/reGenerateSiteSecret.ts index bfa5df9d..09cf7599 100644 --- a/server/private/routers/re-key/reGenerateSiteSecret.ts +++ b/server/private/routers/re-key/reGenerateSiteSecret.ts @@ -13,7 +13,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, newts, sites } from "@server/db"; +import { db, Newt, newts, sites } from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -22,38 +22,19 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { hashPassword } from "@server/auth/password"; -import { addPeer } from "@server/routers/gerbil/peers"; - +import { addPeer, deletePeer } from "@server/routers/gerbil/peers"; +import { getAllowedIps } from "@server/routers/target/helpers"; +import { disconnectClient, sendToClient } from "#private/routers/ws"; const updateSiteParamsSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()) - }); + siteId: z.string().transform(Number).pipe(z.int().positive()) +}); const updateSiteBodySchema = z.strictObject({ - type: z.enum(["newt", "wireguard"]), - newtId: z.string().min(1).max(255).optional(), - newtSecret: z.string().min(1).max(255).optional(), - exitNodeId: z.int().positive().optional(), - pubKey: z.string().optional(), - subnet: z.string().optional(), - }); - -registry.registerPath({ - method: "post", - path: "/re-key/{siteId}/regenerate-site-secret", - description: "Regenerate a site's Newt or WireGuard credentials by its site ID.", - tags: [OpenAPITags.Site], - request: { - params: updateSiteParamsSchema, - body: { - content: { - "application/json": { - schema: updateSiteBodySchema, - }, - }, - }, - }, - responses: {}, + type: z.enum(["newt", "wireguard"]), + secret: z.string().min(1).max(255).optional(), + pubKey: z.string().optional(), + disconnect: z.boolean().optional().default(true) }); export async function reGenerateSiteSecret( @@ -65,74 +46,149 @@ export async function reGenerateSiteSecret( const parsedParams = updateSiteParamsSchema.safeParse(req.params); if (!parsedParams.success) { return next( - createHttpError(HttpCode.BAD_REQUEST, fromError(parsedParams.error).toString()) + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) ); } const parsedBody = updateSiteBodySchema.safeParse(req.body); if (!parsedBody.success) { return next( - createHttpError(HttpCode.BAD_REQUEST, fromError(parsedBody.error).toString()) + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) ); } const { siteId } = parsedParams.data; - const { type, exitNodeId, pubKey, subnet, newtId, newtSecret } = parsedBody.data; - - let updatedSite = undefined; + const { type, pubKey, secret, disconnect } = parsedBody.data; + let existingNewt: Newt | null = null; if (type === "newt") { - if (!newtSecret) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "newtSecret is required for newt sites") - ); - } - - const secretHash = await hashPassword(newtSecret); - - updatedSite = await db - .update(newts) - .set({ - newtId, - secretHash, - }) - .where(eq(newts.siteId, siteId)) - .returning(); - - logger.info(`Regenerated Newt credentials for site ${siteId}`); - - } else if (type === "wireguard") { - if (!pubKey) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Public key is required for wireguard sites") - ); - } - - if (!exitNodeId) { + if (!secret) { return next( createHttpError( HttpCode.BAD_REQUEST, - "Exit node ID is required for wireguard sites" + "newtSecret is required for newt sites" + ) + ); + } + + const secretHash = await hashPassword(secret); + + // get the newt to verify it exists + const existingNewts = await db + .select() + .from(newts) + .where(eq(newts.siteId, siteId)); + + if (existingNewts.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `No Newt found for site ID ${siteId}` + ) + ); + } + + if (existingNewts.length > 1) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Multiple Newts found for site ID ${siteId}` + ) + ); + } + + existingNewt = existingNewts[0]; + + // update the secret on the existing newt + await db + .update(newts) + .set({ + secretHash + }) + .where(eq(newts.newtId, existingNewts[0].newtId)); + + // Only disconnect if explicitly requested + if (disconnect) { + const payload = { + type: `newt/wg/terminate`, + data: {} + }; + // Don't await this to prevent blocking the response + sendToClient(existingNewts[0].newtId, payload).catch( + (error) => { + logger.error( + "Failed to send termination message to newt:", + error + ); + } + ); + + disconnectClient(existingNewts[0].newtId).catch((error) => { + logger.error( + "Failed to disconnect newt after re-key:", + error + ); + }); + } + + logger.info(`Regenerated Newt credentials for site ${siteId}`); + } else if (type === "wireguard") { + if (!pubKey) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Public key is required for wireguard sites" ) ); } try { - updatedSite = await db.transaction(async (tx) => { - await addPeer(exitNodeId, { + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + + if (!site) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${siteId} not found` + ) + ); + } + + await db + .update(sites) + .set({ pubKey }) + .where(eq(sites.siteId, siteId)); + + if (!site) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${siteId} not found` + ) + ); + } + + if (site.exitNodeId && site.subnet) { + await deletePeer(site.exitNodeId, site.pubKey!); // the old pubkey + await addPeer(site.exitNodeId, { publicKey: pubKey, - allowedIps: subnet ? [subnet] : [], + allowedIps: await getAllowedIps(site.siteId) }); - const result = await tx - .update(sites) - .set({ pubKey }) - .where(eq(sites.siteId, siteId)) - .returning(); + } - return result; - }); - - logger.info(`Regenerated WireGuard credentials for site ${siteId}`); + logger.info( + `Regenerated WireGuard credentials for site ${siteId}` + ); } catch (err) { logger.error( `Transaction failed while regenerating WireGuard secret for site ${siteId}`, @@ -148,17 +204,21 @@ export async function reGenerateSiteSecret( } return response(res, { - data: updatedSite, + data: { + newtId: existingNewt ? existingNewt.newtId : undefined + }, success: true, error: false, message: "Credentials regenerated successfully", - status: HttpCode.OK, + status: HttpCode.OK }); - } catch (error) { logger.error("Unexpected error in reGenerateSiteSecret", error); return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An unexpected error occurred") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An unexpected error occurred" + ) ); } } diff --git a/server/private/routers/remoteExitNode/createRemoteExitNode.ts b/server/private/routers/remoteExitNode/createRemoteExitNode.ts index 5afa82ef..f734813e 100644 --- a/server/private/routers/remoteExitNode/createRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/createRemoteExitNode.ts @@ -36,9 +36,9 @@ export const paramsSchema = z.object({ }); const bodySchema = z.strictObject({ - remoteExitNodeId: z.string().length(15), - secret: z.string().length(48) - }); + remoteExitNodeId: z.string().length(15), + secret: z.string().length(48) +}); export type CreateRemoteExitNodeBody = z.infer; diff --git a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts index e293f421..a23363fc 100644 --- a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts @@ -25,9 +25,9 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; const paramsSchema = z.strictObject({ - orgId: z.string().min(1), - remoteExitNodeId: z.string().min(1) - }); + orgId: z.string().min(1), + remoteExitNodeId: z.string().min(1) +}); export async function deleteRemoteExitNode( req: Request, diff --git a/server/private/routers/remoteExitNode/getRemoteExitNode.ts b/server/private/routers/remoteExitNode/getRemoteExitNode.ts index c7b98297..01ea080c 100644 --- a/server/private/routers/remoteExitNode/getRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/getRemoteExitNode.ts @@ -24,9 +24,9 @@ import { fromError } from "zod-validation-error"; import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; const getRemoteExitNodeSchema = z.strictObject({ - orgId: z.string().min(1), - remoteExitNodeId: z.string().min(1) - }); + orgId: z.string().min(1), + remoteExitNodeId: z.string().min(1) +}); async function query(remoteExitNodeId: string) { const [remoteExitNode] = await db diff --git a/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts b/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts index 16ec4d5d..24f0de15 100644 --- a/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts +++ b/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts @@ -55,7 +55,8 @@ export async function getRemoteExitNodeToken( try { if (token) { - const { session, remoteExitNode } = await validateRemoteExitNodeSessionToken(token); + const { session, remoteExitNode } = + await validateRemoteExitNodeSessionToken(token); if (session) { if (config.getRawConfig().app.log_failed_attempts) { logger.info( @@ -103,7 +104,10 @@ export async function getRemoteExitNodeToken( } const resToken = generateSessionToken(); - await createRemoteExitNodeSession(resToken, existingRemoteExitNode.remoteExitNodeId); + await createRemoteExitNodeSession( + resToken, + existingRemoteExitNode.remoteExitNodeId + ); // logger.debug(`Created RemoteExitNode token response: ${JSON.stringify(resToken)}`); diff --git a/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts b/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts index 3e1e130d..dafc1412 100644 --- a/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts +++ b/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts @@ -33,7 +33,9 @@ export const startRemoteExitNodeOfflineChecker = (): void => { offlineCheckerInterval = setInterval(async () => { try { - const twoMinutesAgo = Math.floor((Date.now() - OFFLINE_THRESHOLD_MS) / 1000); + 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 const newlyOfflineNodes = await db @@ -48,11 +50,13 @@ export const startRemoteExitNodeOfflineChecker = (): void => { isNull(exitNodes.lastPing) ) ) - ).returning(); - + ) + .returning(); // Update the sites to offline if they have not pinged either - const exitNodeIds = newlyOfflineNodes.map(node => node.exitNodeId); + const exitNodeIds = newlyOfflineNodes.map( + (node) => node.exitNodeId + ); const sitesOnNode = await db .select() @@ -63,10 +67,10 @@ export const startRemoteExitNodeOfflineChecker = (): void => { inArray(sites.exitNodeId, exitNodeIds) ) ); - + // loop through the sites and process their lastBandwidthUpdate as an iso string and if its more than 1 minute old then mark the site offline for (const site of sitesOnNode) { - if (!site.lastBandwidthUpdate) { + if (!site.lastBandwidthUpdate) { continue; } const lastBandwidthUpdate = new Date(site.lastBandwidthUpdate); @@ -77,13 +81,12 @@ export const startRemoteExitNodeOfflineChecker = (): void => { .where(eq(sites.siteId, site.siteId)); } } - } catch (error) { logger.error("Error in offline checker interval", { error }); } }, OFFLINE_CHECK_INTERVAL); - logger.info("Started offline checker interval"); + logger.debug("Started offline checker interval"); }; /** @@ -100,7 +103,9 @@ export const stopRemoteExitNodeOfflineChecker = (): void => { /** * Handles ping messages from clients and responds with pong */ -export const handleRemoteExitNodePingMessage: MessageHandler = async (context) => { +export const handleRemoteExitNodePingMessage: MessageHandler = async ( + context +) => { const { message, client: c, sendToClient } = context; const remoteExitNode = c as RemoteExitNode; @@ -120,7 +125,7 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (context) = .update(exitNodes) .set({ lastPing: Math.floor(Date.now() / 1000), - online: true, + online: true }) .where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId)); } catch (error) { @@ -131,10 +136,10 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (context) = message: { type: "pong", data: { - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } }, broadcast: false, excludeSender: false }; -}; \ No newline at end of file +}; diff --git a/server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts b/server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts index a733db7d..5ad37edc 100644 --- a/server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts +++ b/server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts @@ -29,7 +29,8 @@ export const handleRemoteExitNodeRegisterMessage: MessageHandler = async ( return; } - const { remoteExitNodeVersion, remoteExitNodeSecondaryVersion } = message.data; + const { remoteExitNodeVersion, remoteExitNodeSecondaryVersion } = + message.data; if (!remoteExitNodeVersion) { logger.warn("Remote exit node version not found"); @@ -39,7 +40,10 @@ export const handleRemoteExitNodeRegisterMessage: MessageHandler = async ( // update the version await db .update(remoteExitNodes) - .set({ version: remoteExitNodeVersion, secondaryVersion: remoteExitNodeSecondaryVersion }) + .set({ + version: remoteExitNodeVersion, + secondaryVersion: remoteExitNodeSecondaryVersion + }) .where( eq( remoteExitNodes.remoteExitNodeId, diff --git a/server/private/routers/remoteExitNode/listRemoteExitNodes.ts b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts index a13a05cd..e6548600 100644 --- a/server/private/routers/remoteExitNode/listRemoteExitNodes.ts +++ b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts @@ -24,8 +24,8 @@ import { fromError } from "zod-validation-error"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; const listRemoteExitNodesParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listRemoteExitNodesSchema = z.object({ limit: z diff --git a/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts index bb7c89d5..5dcd545e 100644 --- a/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts +++ b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts @@ -22,8 +22,8 @@ import { z } from "zod"; import { PickRemoteExitNodeDefaultsResponse } from "@server/routers/remoteExitNode/types"; const paramsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export async function pickRemoteExitNodeDefaults( req: Request, diff --git a/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts b/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts index 4d368152..ebe365d1 100644 --- a/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts @@ -38,7 +38,9 @@ export async function quickStartRemoteExitNode( next: NextFunction ): Promise { try { - const parsedBody = quickStartRemoteExitNodeBodySchema.safeParse(req.body); + const parsedBody = quickStartRemoteExitNodeBodySchema.safeParse( + req.body + ); if (!parsedBody.success) { return next( createHttpError( diff --git a/server/private/routers/ws/index.ts b/server/private/routers/ws/index.ts index 4d803a3a..3a8db537 100644 --- a/server/private/routers/ws/index.ts +++ b/server/private/routers/ws/index.ts @@ -11,4 +11,4 @@ * This file is not licensed under the AGPLv3. */ -export * from "./ws"; \ No newline at end of file +export * from "./ws"; diff --git a/server/private/routers/ws/messageHandlers.ts b/server/private/routers/ws/messageHandlers.ts index 71c2b253..5a6c85cf 100644 --- a/server/private/routers/ws/messageHandlers.ts +++ b/server/private/routers/ws/messageHandlers.ts @@ -23,4 +23,4 @@ export const messageHandlers: Record = { "remoteExitNode/ping": handleRemoteExitNodePingMessage }; -startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes \ No newline at end of file +startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes diff --git a/server/private/routers/ws/ws.ts b/server/private/routers/ws/ws.ts index 41c400cd..784c3d51 100644 --- a/server/private/routers/ws/ws.ts +++ b/server/private/routers/ws/ws.ts @@ -37,7 +37,14 @@ import { validateRemoteExitNodeSessionToken } from "#private/auth/sessions/remot import { rateLimitService } from "#private/lib/rateLimit"; import { messageHandlers } from "@server/routers/ws/messageHandlers"; import { messageHandlers as privateMessageHandlers } from "#private/routers/ws/messageHandlers"; -import { AuthenticatedWebSocket, ClientType, WSMessage, TokenPayload, WebSocketRequest, RedisMessage } from "@server/routers/ws"; +import { + AuthenticatedWebSocket, + ClientType, + WSMessage, + TokenPayload, + WebSocketRequest, + RedisMessage +} from "@server/routers/ws"; import { validateSessionToken } from "@server/auth/sessions/app"; // Merge public and private message handlers @@ -55,9 +62,9 @@ const processMessage = async ( try { const message: WSMessage = JSON.parse(data.toString()); - logger.debug( - `Processing message from ${clientType.toUpperCase()} ID: ${clientId}, type: ${message.type}` - ); + // logger.debug( + // `Processing message from ${clientType.toUpperCase()} ID: ${clientId}, type: ${message.type}` + // ); if (!message.type || typeof message.type !== "string") { throw new Error("Invalid message format: missing or invalid type"); @@ -216,7 +223,7 @@ const initializeRedisSubscription = async (): Promise => { // Each node is responsible for restoring its own connection state to Redis // This approach is more efficient than cross-node coordination because: // 1. Each node knows its own connections (source of truth) -// 2. No network overhead from broadcasting state between nodes +// 2. No network overhead from broadcasting state between nodes // 3. No race conditions from simultaneous updates // 4. Redis becomes eventually consistent as each node restores independently // 5. Simpler logic with better fault tolerance @@ -233,8 +240,10 @@ const recoverConnectionState = async (): Promise => { // Each node simply restores its own local connections to Redis // This is the source of truth - no need for cross-node coordination await restoreLocalConnectionsToRedis(); - - logger.info("Redis connection state recovery completed - restored local state"); + + logger.info( + "Redis connection state recovery completed - restored local state" + ); } catch (error) { logger.error("Error during Redis recovery:", error); } finally { @@ -251,8 +260,10 @@ const restoreLocalConnectionsToRedis = async (): Promise => { try { // Restore all current local connections to Redis for (const [clientId, clients] of connectedClients.entries()) { - const validClients = clients.filter(client => client.readyState === WebSocket.OPEN); - + const validClients = clients.filter( + (client) => client.readyState === WebSocket.OPEN + ); + if (validClients.length > 0) { // Add this node to the client's connection list await redisManager.sadd(getConnectionsKey(clientId), NODE_ID); @@ -303,7 +314,10 @@ const addClient = async ( Date.now().toString() ); } catch (error) { - logger.error("Failed to add client to Redis tracking (connection still functional locally):", error); + logger.error( + "Failed to add client to Redis tracking (connection still functional locally):", + error + ); } } @@ -326,9 +340,14 @@ const removeClient = async ( if (redisManager.isRedisEnabled()) { try { await redisManager.srem(getConnectionsKey(clientId), NODE_ID); - await redisManager.del(getNodeConnectionsKey(NODE_ID, clientId)); + await redisManager.del( + getNodeConnectionsKey(NODE_ID, clientId) + ); } catch (error) { - logger.error("Failed to remove client from Redis tracking (cleanup will occur on recovery):", error); + logger.error( + "Failed to remove client from Redis tracking (cleanup will occur on recovery):", + error + ); } } @@ -345,7 +364,10 @@ const removeClient = async ( ws.connectionId ); } catch (error) { - logger.error("Failed to remove specific connection from Redis tracking:", error); + logger.error( + "Failed to remove specific connection from Redis tracking:", + error + ); } } @@ -372,7 +394,9 @@ const sendToClientLocal = async ( } }); - logger.debug(`sendToClient: Message type ${message.type} sent to clientId ${clientId}`); + logger.debug( + `sendToClient: Message type ${message.type} sent to clientId ${clientId}` + ); return true; }; @@ -411,14 +435,22 @@ const sendToClient = async ( fromNodeId: NODE_ID }; - await redisManager.publish(REDIS_CHANNEL, JSON.stringify(redisMessage)); + await redisManager.publish( + REDIS_CHANNEL, + JSON.stringify(redisMessage) + ); } catch (error) { - logger.error("Failed to send message via Redis, message may be lost:", error); + logger.error( + "Failed to send message via Redis, message may be lost:", + error + ); // Continue execution - local delivery already attempted } } else if (!localSent && !redisManager.isRedisEnabled()) { // Redis is disabled or unavailable - log that we couldn't deliver to remote nodes - logger.debug(`Could not deliver message to ${clientId} - not connected locally and Redis unavailable`); + logger.debug( + `Could not deliver message to ${clientId} - not connected locally and Redis unavailable` + ); } return localSent; @@ -441,13 +473,21 @@ const broadcastToAllExcept = async ( fromNodeId: NODE_ID }; - await redisManager.publish(REDIS_CHANNEL, JSON.stringify(redisMessage)); + await redisManager.publish( + REDIS_CHANNEL, + JSON.stringify(redisMessage) + ); } catch (error) { - logger.error("Failed to broadcast message via Redis, remote nodes may not receive it:", error); + logger.error( + "Failed to broadcast message via Redis, remote nodes may not receive it:", + error + ); // Continue execution - local broadcast already completed } } else { - logger.debug("Redis unavailable - broadcast limited to local node only"); + logger.debug( + "Redis unavailable - broadcast limited to local node only" + ); } }; @@ -512,8 +552,10 @@ const verifyToken = async ( return null; } - if (olm.userId) { // this is a user device and we need to check the user token - const { session: userSession, user } = await validateSessionToken(userToken); + if (olm.userId) { + // this is a user device and we need to check the user token + const { session: userSession, user } = + await validateSessionToken(userToken); if (!userSession || !user) { return null; } @@ -668,7 +710,7 @@ const handleWSUpgrade = (server: HttpServer): void => { url.searchParams.get("token") || request.headers["sec-websocket-protocol"] || ""; - const userToken = url.searchParams.get('userToken') || ''; + const userToken = url.searchParams.get("userToken") || ""; let clientType = url.searchParams.get( "clientType" ) as ClientType; @@ -690,7 +732,11 @@ const handleWSUpgrade = (server: HttpServer): void => { return; } - const tokenPayload = await verifyToken(token, clientType, userToken); + const tokenPayload = await verifyToken( + token, + clientType, + userToken + ); if (!tokenPayload) { logger.debug( "Unauthorized connection attempt: invalid token..." @@ -724,50 +770,68 @@ const handleWSUpgrade = (server: HttpServer): void => { // Add periodic connection state sync to handle Redis disconnections/reconnections const startPeriodicStateSync = (): void => { // Lightweight sync every 5 minutes - just restore our own state - setInterval(async () => { - if (redisManager.isRedisEnabled() && !isRedisRecoveryInProgress) { - try { - await restoreLocalConnectionsToRedis(); - logger.debug("Periodic connection state sync completed"); - } catch (error) { - logger.error("Error during periodic connection state sync:", error); + setInterval( + async () => { + if (redisManager.isRedisEnabled() && !isRedisRecoveryInProgress) { + try { + await restoreLocalConnectionsToRedis(); + logger.debug("Periodic connection state sync completed"); + } catch (error) { + logger.error( + "Error during periodic connection state sync:", + error + ); + } } - } - }, 5 * 60 * 1000); // 5 minutes + }, + 5 * 60 * 1000 + ); // 5 minutes // Cleanup stale connections every 15 minutes - setInterval(async () => { - if (redisManager.isRedisEnabled()) { - try { - await cleanupStaleConnections(); - logger.debug("Periodic connection cleanup completed"); - } catch (error) { - logger.error("Error during periodic connection cleanup:", error); + setInterval( + async () => { + if (redisManager.isRedisEnabled()) { + try { + await cleanupStaleConnections(); + logger.debug("Periodic connection cleanup completed"); + } catch (error) { + logger.error( + "Error during periodic connection cleanup:", + error + ); + } } - } - }, 15 * 60 * 1000); // 15 minutes + }, + 15 * 60 * 1000 + ); // 15 minutes }; const cleanupStaleConnections = async (): Promise => { if (!redisManager.isRedisEnabled()) return; try { - const nodeKeys = await redisManager.getClient()?.keys(`ws:node:${NODE_ID}:*`) || []; - + const nodeKeys = + (await redisManager.getClient()?.keys(`ws:node:${NODE_ID}:*`)) || + []; + for (const nodeKey of nodeKeys) { const connections = await redisManager.hgetall(nodeKey); - const clientId = nodeKey.replace(`ws:node:${NODE_ID}:`, ''); + const clientId = nodeKey.replace(`ws:node:${NODE_ID}:`, ""); const localClients = connectedClients.get(clientId) || []; const localConnectionIds = localClients - .filter(client => client.readyState === WebSocket.OPEN) - .map(client => client.connectionId) + .filter((client) => client.readyState === WebSocket.OPEN) + .map((client) => client.connectionId) .filter(Boolean); // Remove Redis entries for connections that no longer exist locally - for (const [connectionId, timestamp] of Object.entries(connections)) { + for (const [connectionId, timestamp] of Object.entries( + connections + )) { if (!localConnectionIds.includes(connectionId)) { await redisManager.hdel(nodeKey, connectionId); - logger.debug(`Cleaned up stale connection: ${connectionId} for client: ${clientId}`); + logger.debug( + `Cleaned up stale connection: ${connectionId} for client: ${clientId}` + ); } } @@ -776,7 +840,9 @@ const cleanupStaleConnections = async (): Promise => { if (Object.keys(remainingConnections).length === 0) { await redisManager.srem(getConnectionsKey(clientId), NODE_ID); await redisManager.del(nodeKey); - logger.debug(`Cleaned up empty connection tracking for client: ${clientId}`); + logger.debug( + `Cleaned up empty connection tracking for client: ${clientId}` + ); } } } catch (error) { @@ -789,38 +855,38 @@ if (redisManager.isRedisEnabled()) { initializeRedisSubscription().catch((error) => { logger.error("Failed to initialize Redis subscription:", error); }); - + // Register recovery callback with Redis manager // When Redis reconnects, each node simply restores its own local state redisManager.onReconnection(async () => { logger.info("Redis reconnected, starting WebSocket state recovery..."); await recoverConnectionState(); }); - + // Start periodic state synchronization startPeriodicStateSync(); - + logger.info( `WebSocket handler initialized with Redis support - Node ID: ${NODE_ID}` ); } else { - logger.debug( - "WebSocket handler initialized in local mode" - ); + logger.debug("WebSocket handler initialized in local mode"); } // Disconnect a specific client and force them to reconnect const disconnectClient = async (clientId: string): Promise => { const mapKey = getClientMapKey(clientId); const clients = connectedClients.get(mapKey); - + if (!clients || clients.length === 0) { logger.debug(`No connections found for client ID: ${clientId}`); return false; } - logger.info(`Disconnecting client ID: ${clientId} (${clients.length} connection(s))`); - + logger.info( + `Disconnecting client ID: ${clientId} (${clients.length} connection(s))` + ); + // Close all connections for this client clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { diff --git a/server/routers/accessToken/deleteAccessToken.ts b/server/routers/accessToken/deleteAccessToken.ts index 5de4df9b..4e18ddeb 100644 --- a/server/routers/accessToken/deleteAccessToken.ts +++ b/server/routers/accessToken/deleteAccessToken.ts @@ -11,8 +11,8 @@ import { db } from "@server/db"; import { OpenAPITags, registry } from "@server/openApi"; const deleteAccessTokenParamsSchema = z.strictObject({ - accessTokenId: z.string() - }); + accessTokenId: z.string() +}); registry.registerPath({ method: "delete", diff --git a/server/routers/accessToken/generateAccessToken.ts b/server/routers/accessToken/generateAccessToken.ts index 36a20268..35da6add 100644 --- a/server/routers/accessToken/generateAccessToken.ts +++ b/server/routers/accessToken/generateAccessToken.ts @@ -25,17 +25,14 @@ import { sha256 } from "@oslojs/crypto/sha2"; import { OpenAPITags, registry } from "@server/openApi"; export const generateAccessTokenBodySchema = z.strictObject({ - validForSeconds: z.int().positive().optional(), // seconds - title: z.string().optional(), - description: z.string().optional() - }); + validForSeconds: z.int().positive().optional(), // seconds + title: z.string().optional(), + description: z.string().optional() +}); export const generateAccssTokenParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); export type GenerateAccessTokenResponse = Omit< ResourceAccessToken, diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts index 476c858b..2f929fc6 100644 --- a/server/routers/accessToken/listAccessTokens.ts +++ b/server/routers/accessToken/listAccessTokens.ts @@ -17,7 +17,8 @@ import stoi from "@server/lib/stoi"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -const listAccessTokensParamsSchema = z.strictObject({ +const listAccessTokensParamsSchema = z + .strictObject({ resourceId: z .string() .optional() diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts index 8e9e571d..fc076623 100644 --- a/server/routers/apiKeys/createRootApiKey.ts +++ b/server/routers/apiKeys/createRootApiKey.ts @@ -15,8 +15,8 @@ import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; const bodySchema = z.strictObject({ - name: z.string().min(1).max(255) - }); + name: z.string().min(1).max(255) +}); export type CreateRootApiKeyBody = z.infer; diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts index 7432d175..073a7583 100644 --- a/server/routers/apiKeys/listApiKeyActions.ts +++ b/server/routers/apiKeys/listApiKeyActions.ts @@ -47,8 +47,7 @@ export type ListApiKeyActionsResponse = { registry.registerPath({ method: "get", path: "/org/{orgId}/api-key/{apiKeyId}/actions", - description: - "List all actions set for an API key.", + description: "List all actions set for an API key.", tags: [OpenAPITags.Org, OpenAPITags.ApiKey], request: { params: paramsSchema, diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts index fe8cc4f1..62967388 100644 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ b/server/routers/apiKeys/setApiKeyActions.ts @@ -11,9 +11,10 @@ import { eq, and, inArray } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const bodySchema = z.strictObject({ - actionIds: z.tuple([z.string()], z.string()) - .transform((v) => Array.from(new Set(v))) - }); + actionIds: z + .tuple([z.string()], z.string()) + .transform((v) => Array.from(new Set(v))) +}); const paramsSchema = z.object({ apiKeyId: z.string().nonempty() diff --git a/server/routers/apiKeys/setApiKeyOrgs.ts b/server/routers/apiKeys/setApiKeyOrgs.ts index d60aad73..51d0f043 100644 --- a/server/routers/apiKeys/setApiKeyOrgs.ts +++ b/server/routers/apiKeys/setApiKeyOrgs.ts @@ -10,9 +10,10 @@ import { fromError } from "zod-validation-error"; import { eq, and, inArray } from "drizzle-orm"; const bodySchema = z.strictObject({ - orgIds: z.tuple([z.string()], z.string()) - .transform((v) => Array.from(new Set(v))) - }); + orgIds: z + .tuple([z.string()], z.string()) + .transform((v) => Array.from(new Set(v))) +}); const paramsSchema = z.object({ apiKeyId: z.string().nonempty() diff --git a/server/routers/auditLogs/generateCSV.ts b/server/routers/auditLogs/generateCSV.ts index 8a067069..ea0da29f 100644 --- a/server/routers/auditLogs/generateCSV.ts +++ b/server/routers/auditLogs/generateCSV.ts @@ -2,15 +2,17 @@ export function generateCSV(data: any[]): string { if (data.length === 0) { return "orgId,action,actorType,timestamp,actor\n"; } - + const headers = Object.keys(data[0]).join(","); - const rows = data.map(row => - Object.values(row).map(value => - typeof value === 'string' && value.includes(',') - ? `"${value.replace(/"/g, '""')}"` - : value - ).join(",") + const rows = data.map((row) => + Object.values(row) + .map((value) => + typeof value === "string" && value.includes(",") + ? `"${value.replace(/"/g, '""')}"` + : value + ) + .join(",") ); - + return [headers, ...rows].join("\n"); -} \ No newline at end of file +} diff --git a/server/routers/auditLogs/types.ts b/server/routers/auditLogs/types.ts index 81cef733..474aa926 100644 --- a/server/routers/auditLogs/types.ts +++ b/server/routers/auditLogs/types.ts @@ -90,4 +90,4 @@ export type QueryAccessAuditLogResponse = { }[]; locations: string[]; }; -}; \ No newline at end of file +}; diff --git a/server/routers/auth/changePassword.ts b/server/routers/auth/changePassword.ts index fa007d37..1a26b911 100644 --- a/server/routers/auth/changePassword.ts +++ b/server/routers/auth/changePassword.ts @@ -6,10 +6,7 @@ import { z } from "zod"; import { db } from "@server/db"; import { User, users } from "@server/db"; import { response } from "@server/lib/response"; -import { - hashPassword, - verifyPassword -} from "@server/auth/password"; +import { hashPassword, verifyPassword } from "@server/auth/password"; import { verifyTotpCode } from "@server/auth/totp"; import logger from "@server/logger"; import { unauthorized } from "@server/auth/unauthorizedResponse"; @@ -23,10 +20,10 @@ import ConfirmPasswordReset from "@server/emails/templates/NotifyResetPassword"; import config from "@server/lib/config"; export const changePasswordBody = z.strictObject({ - oldPassword: z.string(), - newPassword: passwordSchema, - code: z.string().optional() - }); + oldPassword: z.string(), + newPassword: passwordSchema, + code: z.string().optional() +}); export type ChangePasswordBody = z.infer; @@ -62,12 +59,14 @@ async function invalidateAllSessionsExceptCurrent( } // Delete the user sessions (except current) - await trx.delete(sessions).where( - and( - eq(sessions.userId, userId), - ne(sessions.sessionId, currentSessionId) - ) - ); + await trx + .delete(sessions) + .where( + and( + eq(sessions.userId, userId), + ne(sessions.sessionId, currentSessionId) + ) + ); }); } catch (e) { logger.error("Failed to invalidate user sessions except current", e); @@ -157,7 +156,10 @@ export async function changePassword( .where(eq(users.userId, user.userId)); // Invalidate all sessions except the current one - await invalidateAllSessionsExceptCurrent(user.userId, req.session.sessionId); + await invalidateAllSessionsExceptCurrent( + user.userId, + req.session.sessionId + ); try { const email = user.email!; diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts index 39466400..74a94a84 100644 --- a/server/routers/auth/checkResourceSession.ts +++ b/server/routers/auth/checkResourceSession.ts @@ -9,7 +9,7 @@ import logger from "@server/logger"; export const params = z.strictObject({ token: z.string(), - resourceId: z.string().transform(Number).pipe(z.int().positive()), + resourceId: z.string().transform(Number).pipe(z.int().positive()) }); export type CheckResourceSessionParams = z.infer; @@ -21,7 +21,7 @@ export type CheckResourceSessionResponse = { export async function checkResourceSession( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise { const parsedParams = params.safeParse(req.params); @@ -29,8 +29,8 @@ export async function checkResourceSession( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString(), - ), + fromError(parsedParams.error).toString() + ) ); } @@ -39,7 +39,7 @@ export async function checkResourceSession( try { const { resourceSession } = await validateResourceSessionToken( token, - resourceId, + resourceId ); let valid = false; @@ -52,15 +52,15 @@ export async function checkResourceSession( success: true, error: false, message: "Checked validity", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (e) { logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "Failed to reset password", - ), + "Failed to reset password" + ) ); } } diff --git a/server/routers/auth/disable2fa.ts b/server/routers/auth/disable2fa.ts index ebf6ab52..254d6ccd 100644 --- a/server/routers/auth/disable2fa.ts +++ b/server/routers/auth/disable2fa.ts @@ -17,9 +17,9 @@ import { unauthorized } from "@server/auth/unauthorizedResponse"; import { UserType } from "@server/types/UserTypes"; export const disable2faBody = z.strictObject({ - password: z.string(), - code: z.string().optional() - }); + password: z.string(), + code: z.string().optional() +}); export type Disable2faBody = z.infer; @@ -56,7 +56,10 @@ export async function disable2fa( } try { - const validPassword = await verifyPassword(password, user.passwordHash!); + const validPassword = await verifyPassword( + password, + user.passwordHash! + ); if (!validPassword) { return next(unauthorized()); } diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index 4600a4cc..22040614 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -16,4 +16,4 @@ export * from "./checkResourceSession"; export * from "./securityKey"; export * from "./startDeviceWebAuth"; export * from "./verifyDeviceWebAuth"; -export * from "./pollDeviceWebAuth"; \ No newline at end of file +export * from "./pollDeviceWebAuth"; diff --git a/server/routers/auth/pollDeviceWebAuth.ts b/server/routers/auth/pollDeviceWebAuth.ts index 9949ab42..a5c71362 100644 --- a/server/routers/auth/pollDeviceWebAuth.ts +++ b/server/routers/auth/pollDeviceWebAuth.ts @@ -7,10 +7,7 @@ import logger from "@server/logger"; import { response } from "@server/lib/response"; import { db, deviceWebAuthCodes } from "@server/db"; import { eq, and, gt } from "drizzle-orm"; -import { - createSession, - generateSessionToken -} from "@server/auth/sessions/app"; +import { createSession, generateSessionToken } from "@server/auth/sessions/app"; import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; @@ -22,9 +19,7 @@ export type PollDeviceWebAuthParams = z.infer; // Helper function to hash device code before querying database function hashDeviceCode(code: string): string { - return encodeHexLowerCase( - sha256(new TextEncoder().encode(code)) - ); + return encodeHexLowerCase(sha256(new TextEncoder().encode(code))); } export type PollDeviceWebAuthResponse = { @@ -127,7 +122,9 @@ export async function pollDeviceWebAuth( // Check if userId is set (should be set when verified) if (!deviceCode.userId) { - logger.error("Device code is verified but userId is missing", { codeId: deviceCode.codeId }); + logger.error("Device code is verified but userId is missing", { + codeId: deviceCode.codeId + }); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, @@ -165,4 +162,3 @@ export async function pollDeviceWebAuth( ); } } - diff --git a/server/routers/auth/requestPasswordReset.ts b/server/routers/auth/requestPasswordReset.ts index 0f9953e8..42b53d24 100644 --- a/server/routers/auth/requestPasswordReset.ts +++ b/server/routers/auth/requestPasswordReset.ts @@ -18,8 +18,8 @@ import { hashPassword } from "@server/auth/password"; import { UserType } from "@server/types/UserTypes"; export const requestPasswordResetBody = z.strictObject({ - email: z.email().toLowerCase() - }); + email: z.email().toLowerCase() +}); export type RequestPasswordResetBody = z.infer; diff --git a/server/routers/auth/requestTotpSecret.ts b/server/routers/auth/requestTotpSecret.ts index 53d80147..bc032ecd 100644 --- a/server/routers/auth/requestTotpSecret.ts +++ b/server/routers/auth/requestTotpSecret.ts @@ -17,9 +17,9 @@ import { verifySession } from "@server/auth/sessions/verifySession"; import config from "@server/lib/config"; export const requestTotpSecretBody = z.strictObject({ - password: z.string(), - email: z.email().optional() - }); + password: z.string(), + email: z.email().optional() +}); export type RequestTotpSecretBody = z.infer; @@ -46,7 +46,8 @@ export async function requestTotpSecret( const { password, email } = parsedBody.data; - const { user: sessionUser, session: existingSession } = await verifySession(req); + const { user: sessionUser, session: existingSession } = + await verifySession(req); let user: User | null = sessionUser; if (!existingSession) { @@ -112,11 +113,7 @@ export async function requestTotpSecret( const hex = crypto.getRandomValues(new Uint8Array(20)); const secret = encodeHex(hex); - const uri = createTOTPKeyURI( - appName, - user.email!, - hex - ); + const uri = createTOTPKeyURI(appName, user.email!, hex); await db .update(users) diff --git a/server/routers/auth/resetPassword.ts b/server/routers/auth/resetPassword.ts index aeb85558..6e616346 100644 --- a/server/routers/auth/resetPassword.ts +++ b/server/routers/auth/resetPassword.ts @@ -18,11 +18,11 @@ import { sendEmail } from "@server/emails"; import { passwordSchema } from "@server/auth/passwordSchema"; export const resetPasswordBody = z.strictObject({ - email: z.email().toLowerCase(), - token: z.string(), // reset secret code - newPassword: passwordSchema, - code: z.string().optional() // 2fa code - }); + email: z.email().toLowerCase(), + token: z.string(), // reset secret code + newPassword: passwordSchema, + code: z.string().optional() // 2fa code +}); export type ResetPasswordBody = z.infer; diff --git a/server/routers/auth/securityKey.ts b/server/routers/auth/securityKey.ts index eed2328d..9a1ee2cd 100644 --- a/server/routers/auth/securityKey.ts +++ b/server/routers/auth/securityKey.ts @@ -19,9 +19,7 @@ import type { GenerateAuthenticationOptionsOpts, AuthenticatorTransportFuture } from "@simplewebauthn/server"; -import { - isoBase64URL -} from '@simplewebauthn/server/helpers'; +import { isoBase64URL } from "@simplewebauthn/server/helpers"; import config from "@server/lib/config"; import { UserType } from "@server/types/UserTypes"; import { verifyPassword } from "@server/auth/password"; @@ -30,10 +28,12 @@ import { verifyTotpCode } from "@server/auth/totp"; // The RP ID is the domain name of your application const rpID = (() => { - const url = config.getRawConfig().app.dashboard_url ? new URL(config.getRawConfig().app.dashboard_url!) : undefined; + 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' || !url) { - return 'localhost'; + if (url?.hostname === "localhost" || !url) { + return "localhost"; } return url.hostname; })(); @@ -46,25 +46,38 @@ const origin = config.getRawConfig().app.dashboard_url || "localhost"; // This supports clustered deployments and persists across server restarts // Clean up expired challenges every 5 minutes -setInterval(async () => { - try { - const now = Date.now(); - await db - .delete(webauthnChallenge) - .where(lt(webauthnChallenge.expiresAt, now)); - // logger.debug("Cleaned up expired security key challenges"); - } catch (error) { - logger.error("Failed to clean up expired security key challenges", error); - } -}, 5 * 60 * 1000); +setInterval( + async () => { + try { + const now = Date.now(); + await db + .delete(webauthnChallenge) + .where(lt(webauthnChallenge.expiresAt, now)); + // logger.debug("Cleaned up expired security key challenges"); + } catch (error) { + logger.error( + "Failed to clean up expired security key challenges", + error + ); + } + }, + 5 * 60 * 1000 +); // Helper functions for challenge management -async function storeChallenge(sessionId: string, challenge: string, securityKeyName?: string, userId?: string) { - const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes - +async function storeChallenge( + sessionId: string, + challenge: string, + securityKeyName?: string, + userId?: string +) { + const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes + // Delete any existing challenge for this session - await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); - + await db + .delete(webauthnChallenge) + .where(eq(webauthnChallenge.sessionId, sessionId)); + // Insert new challenge await db.insert(webauthnChallenge).values({ sessionId, @@ -88,7 +101,9 @@ async function getChallenge(sessionId: string) { // Check if expired if (challengeData.expiresAt < Date.now()) { - await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); + await db + .delete(webauthnChallenge) + .where(eq(webauthnChallenge.sessionId, sessionId)); return null; } @@ -96,7 +111,9 @@ async function getChallenge(sessionId: string) { } async function clearChallenge(sessionId: string) { - await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); + await db + .delete(webauthnChallenge) + .where(eq(webauthnChallenge.sessionId, sessionId)); } export const registerSecurityKeyBody = z.strictObject({ @@ -153,7 +170,10 @@ export async function startRegistration( try { // Verify password - const validPassword = await verifyPassword(password, user.passwordHash!); + const validPassword = await verifyPassword( + password, + user.passwordHash! + ); if (!validPassword) { return next(unauthorized()); } @@ -197,9 +217,11 @@ export async function startRegistration( .from(securityKeys) .where(eq(securityKeys.userId, user.userId)); - const excludeCredentials = existingSecurityKeys.map(key => ({ + const excludeCredentials = existingSecurityKeys.map((key) => ({ id: key.credentialId, - transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined + transports: key.transports + ? (JSON.parse(key.transports) as AuthenticatorTransportFuture[]) + : undefined })); const options: GenerateRegistrationOptionsOpts = { @@ -207,18 +229,23 @@ export async function startRegistration( rpID, userID: isoBase64URL.toBuffer(user.userId), userName: user.email || user.username, - attestationType: 'none', + attestationType: "none", excludeCredentials, authenticatorSelection: { - residentKey: 'preferred', - userVerification: 'preferred', + residentKey: "preferred", + userVerification: "preferred" } }; const registrationOptions = await generateRegistrationOptions(options); // Store challenge in database - await storeChallenge(req.session.sessionId, registrationOptions.challenge, name, user.userId); + await storeChallenge( + req.session.sessionId, + registrationOptions.challenge, + name, + user.userId + ); return response(res, { data: registrationOptions, @@ -270,7 +297,7 @@ export async function verifyRegistration( try { // Get challenge from database const challengeData = await getChallenge(req.session.sessionId); - + if (!challengeData) { return next( createHttpError( @@ -292,10 +319,7 @@ export async function verifyRegistration( if (!verified || !registrationInfo) { return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Verification failed" - ) + createHttpError(HttpCode.BAD_REQUEST, "Verification failed") ); } @@ -303,9 +327,13 @@ export async function verifyRegistration( await db.insert(securityKeys).values({ credentialId: registrationInfo.credential.id, userId: user.userId, - publicKey: isoBase64URL.fromBuffer(registrationInfo.credential.publicKey), + publicKey: isoBase64URL.fromBuffer( + registrationInfo.credential.publicKey + ), signCount: registrationInfo.credential.counter || 0, - transports: registrationInfo.credential.transports ? JSON.stringify(registrationInfo.credential.transports) : null, + transports: registrationInfo.credential.transports + ? JSON.stringify(registrationInfo.credential.transports) + : null, name: challengeData.securityKeyName, lastUsed: new Date().toISOString(), dateCreated: new Date().toISOString() @@ -407,7 +435,10 @@ export async function deleteSecurityKey( try { // Verify password - const validPassword = await verifyPassword(password, user.passwordHash!); + const validPassword = await verifyPassword( + password, + user.passwordHash! + ); if (!validPassword) { return next(unauthorized()); } @@ -447,10 +478,12 @@ export async function deleteSecurityKey( await db .delete(securityKeys) - .where(and( - eq(securityKeys.credentialId, credentialId), - eq(securityKeys.userId, user.userId) - )); + .where( + and( + eq(securityKeys.credentialId, credentialId), + eq(securityKeys.userId, user.userId) + ) + ); return response(res, { data: null, @@ -502,10 +535,7 @@ export async function startAuthentication( if (!user || user.type !== UserType.Internal) { return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid credentials" - ) + createHttpError(HttpCode.BAD_REQUEST, "Invalid credentials") ); } @@ -525,25 +555,37 @@ export async function startAuthentication( ); } - allowCredentials = userSecurityKeys.map(key => ({ + allowCredentials = userSecurityKeys.map((key) => ({ id: key.credentialId, - transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined + transports: key.transports + ? (JSON.parse( + key.transports + ) as AuthenticatorTransportFuture[]) + : undefined })); } const options: GenerateAuthenticationOptionsOpts = { rpID, allowCredentials, - userVerification: 'preferred', + userVerification: "preferred" }; - const authenticationOptions = await generateAuthenticationOptions(options); + const authenticationOptions = + await generateAuthenticationOptions(options); // Generate a temporary session ID for unauthenticated users - const tempSessionId = email ? `temp_${email}_${Date.now()}` : `temp_${Date.now()}`; + const tempSessionId = email + ? `temp_${email}_${Date.now()}` + : `temp_${Date.now()}`; // Store challenge in database - await storeChallenge(tempSessionId, authenticationOptions.challenge, undefined, userId); + await storeChallenge( + tempSessionId, + authenticationOptions.challenge, + undefined, + userId + ); return response(res, { data: { ...authenticationOptions, tempSessionId }, @@ -580,7 +622,7 @@ export async function verifyAuthentication( } const { credential } = parsedBody.data; - const tempSessionId = req.headers['x-temp-session-id'] as string; + const tempSessionId = req.headers["x-temp-session-id"] as string; if (!tempSessionId) { return next( @@ -594,7 +636,7 @@ export async function verifyAuthentication( try { // Get challenge from database const challengeData = await getChallenge(tempSessionId); - + if (!challengeData) { return next( createHttpError( @@ -646,7 +688,11 @@ export async function verifyAuthentication( id: securityKey.credentialId, publicKey: isoBase64URL.toBuffer(securityKey.publicKey), counter: securityKey.signCount, - transports: securityKey.transports ? JSON.parse(securityKey.transports) as AuthenticatorTransportFuture[] : undefined + transports: securityKey.transports + ? (JSON.parse( + securityKey.transports + ) as AuthenticatorTransportFuture[]) + : undefined }, requireUserVerification: false }); @@ -672,7 +718,8 @@ export async function verifyAuthentication( .where(eq(securityKeys.credentialId, credentialId)); // Create session for the user - const { createSession, generateSessionToken, serializeSessionCookie } = await import("@server/auth/sessions/app"); + const { createSession, generateSessionToken, serializeSessionCookie } = + await import("@server/auth/sessions/app"); const token = generateSessionToken(); const session = await createSession(token, user.userId); const isSecure = req.protocol === "https"; @@ -703,4 +750,4 @@ export async function verifyAuthentication( ) ); } -} \ No newline at end of file +} diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 842214cf..2605a026 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -56,8 +56,14 @@ export async function signup( ); } - const { email, password, inviteToken, inviteId, termsAcceptedTimestamp, marketingEmailConsent } = - parsedBody.data; + const { + email, + password, + inviteToken, + inviteId, + termsAcceptedTimestamp, + marketingEmailConsent + } = parsedBody.data; const passwordHash = await hashPassword(password); const userId = generateId(15); @@ -222,7 +228,9 @@ export async function signup( ); res.appendHeader("Set-Cookie", cookie); if (build == "saas" && marketingEmailConsent) { - logger.debug(`User ${email} opted in to marketing emails during signup.`); + logger.debug( + `User ${email} opted in to marketing emails during signup.` + ); moveEmailToAudience(email, AudienceIds.SignUps); } diff --git a/server/routers/auth/startDeviceWebAuth.ts b/server/routers/auth/startDeviceWebAuth.ts index 925df67f..85fb5262 100644 --- a/server/routers/auth/startDeviceWebAuth.ts +++ b/server/routers/auth/startDeviceWebAuth.ts @@ -13,10 +13,12 @@ import { maxmindLookup } from "@server/db/maxmind"; import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -const bodySchema = z.object({ - deviceName: z.string().optional(), - applicationName: z.string().min(1, "Application name is required") -}).strict(); +const bodySchema = z + .object({ + deviceName: z.string().optional(), + applicationName: z.string().min(1, "Application name is required") + }) + .strict(); export type StartDeviceWebAuthBody = z.infer; @@ -34,14 +36,12 @@ function generateDeviceCode(): string { // Helper function to hash device code before storing in database function hashDeviceCode(code: string): string { - return encodeHexLowerCase( - sha256(new TextEncoder().encode(code)) - ); + return encodeHexLowerCase(sha256(new TextEncoder().encode(code))); } // Helper function to extract IP from request function extractIpFromRequest(req: Request): string | undefined { - const ip = req.ip || req.socket.remoteAddress; + const ip = req.ip; if (!ip) { return undefined; } @@ -75,10 +75,10 @@ async function getCityFromIp(ip: string): Promise { return undefined; } - // MaxMind CountryResponse doesn't include city by default - // If city data is available, it would be in result.city?.names?.en - // But since we're using CountryResponse type, we'll just return undefined - // The user said "don't do this if not easy", so we'll skip city for now + if (result.country) { + return result.country.names?.en || result.country.iso_code; + } + return undefined; } catch (error) { logger.debug("Failed to get city from IP", error); diff --git a/server/routers/auth/types.ts b/server/routers/auth/types.ts index bb5a1b4e..023b2d8e 100644 --- a/server/routers/auth/types.ts +++ b/server/routers/auth/types.ts @@ -5,4 +5,4 @@ export type TransferSessionResponse = { export type GetSessionTransferTokenRenponse = { token: string; -}; \ No newline at end of file +}; diff --git a/server/routers/auth/validateSetupToken.ts b/server/routers/auth/validateSetupToken.ts index 1a4725b6..26043f2d 100644 --- a/server/routers/auth/validateSetupToken.ts +++ b/server/routers/auth/validateSetupToken.ts @@ -9,8 +9,8 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const validateSetupTokenSchema = z.strictObject({ - token: z.string().min(1, "Token is required") - }); + token: z.string().min(1, "Token is required") +}); export type ValidateSetupTokenResponse = { valid: boolean; @@ -41,10 +41,7 @@ export async function validateSetupToken( .select() .from(setupTokens) .where( - and( - eq(setupTokens.token, token), - eq(setupTokens.used, false) - ) + and(eq(setupTokens.token, token), eq(setupTokens.used, false)) ); if (!setupToken) { @@ -79,4 +76,4 @@ export async function validateSetupToken( ) ); } -} \ No newline at end of file +} diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts index 8d31eb45..31c5166d 100644 --- a/server/routers/auth/verifyEmail.ts +++ b/server/routers/auth/verifyEmail.ts @@ -14,8 +14,8 @@ import { freeLimitSet, limitsService } from "@server/lib/billing"; import { build } from "@server/build"; export const verifyEmailBody = z.strictObject({ - code: z.string() - }); + code: z.string() +}); export type VerifyEmailBody = z.infer; diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index 9243c9f9..207287ea 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -19,10 +19,10 @@ import { verifySession } from "@server/auth/sessions/verifySession"; import { unauthorized } from "@server/auth/unauthorizedResponse"; export const verifyTotpBody = z.strictObject({ - email: z.email().optional(), - password: z.string().optional(), - code: z.string() - }); + email: z.email().optional(), + password: z.string().optional(), + code: z.string() +}); export type VerifyTotpBody = z.infer; diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index b4b2deea..b8d01c11 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -12,7 +12,10 @@ import { serializeResourceSessionCookie, validateResourceSessionToken } from "@server/auth/sessions/resource"; -import { generateSessionToken, SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app"; +import { + generateSessionToken, + SESSION_COOKIE_EXPIRES +} from "@server/auth/sessions/app"; import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource"; import config from "@server/lib/config"; import { response } from "@server/lib/response"; @@ -55,8 +58,8 @@ export async function exchangeSession( let cleanHost = host; // if the host ends with :port if (cleanHost.match(/:[0-9]{1,5}$/)) { - const matched = ''+cleanHost.match(/:[0-9]{1,5}$/); - cleanHost = cleanHost.slice(0, -1*matched.length); + const matched = "" + cleanHost.match(/:[0-9]{1,5}$/); + cleanHost = cleanHost.slice(0, -1 * matched.length); } const clientIp = requestIp?.split(":")[0]; @@ -153,8 +156,8 @@ export async function exchangeSession( } } else { const expires = new Date( - Date.now() + SESSION_COOKIE_EXPIRES - ).getTime(); + Date.now() + SESSION_COOKIE_EXPIRES + ).getTime(); await createResourceSession({ token, resourceId: resource.resourceId, diff --git a/server/routers/badger/logRequestAudit.ts b/server/routers/badger/logRequestAudit.ts index 70f1fe1a..1cf97f98 100644 --- a/server/routers/badger/logRequestAudit.ts +++ b/server/routers/badger/logRequestAudit.ts @@ -2,6 +2,7 @@ import { db, orgs, requestAuditLog } from "@server/db"; import logger from "@server/logger"; import { and, eq, lt } from "drizzle-orm"; import cache from "@server/lib/cache"; +import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; /** @@ -24,6 +25,78 @@ Reasons: */ +// In-memory buffer for batching audit logs +const auditLogBuffer: Array<{ + timestamp: number; + orgId?: string; + actorType?: string; + actor?: string; + actorId?: string; + metadata: any; + action: boolean; + resourceId?: number; + reason: number; + location?: string; + originalRequestURL: string; + scheme: string; + host: string; + path: string; + method: string; + ip?: string; + tls: boolean; +}> = []; + +const BATCH_SIZE = 100; // Write to DB every 100 logs +const BATCH_INTERVAL_MS = 5000; // Or every 5 seconds, whichever comes first +let flushTimer: NodeJS.Timeout | null = null; + +/** + * Flush buffered logs to database + */ +async function flushAuditLogs() { + if (auditLogBuffer.length === 0) { + return; + } + + // Take all current logs and clear buffer + const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length); + + try { + // Batch insert all logs at once + await db.insert(requestAuditLog).values(logsToWrite); + logger.debug(`Flushed ${logsToWrite.length} audit logs to database`); + } catch (error) { + logger.error("Error flushing audit logs:", error); + // On error, we lose these logs - consider a fallback strategy if needed + // (e.g., write to file, or put back in buffer with retry limit) + } +} + +/** + * Schedule a flush if not already scheduled + */ +function scheduleFlush() { + if (flushTimer === null) { + flushTimer = setTimeout(() => { + flushTimer = null; + flushAuditLogs().catch((err) => + logger.error("Error in scheduled flush:", err) + ); + }, BATCH_INTERVAL_MS); + } +} + +/** + * Gracefully flush all pending logs (call this on shutdown) + */ +export async function shutdownAuditLogger() { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } + await flushAuditLogs(); +} + async function getRetentionDays(orgId: string): Promise { // check cache first const cached = cache.get(`org_${orgId}_retentionDays`); @@ -55,9 +128,7 @@ async function getRetentionDays(orgId: string): Promise { } export async function cleanUpOldLogs(orgId: string, retentionDays: number) { - const now = Math.floor(Date.now() / 1000); - - const cutoffTimestamp = now - retentionDays * 24 * 60 * 60; + const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); try { await db @@ -77,7 +148,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) { } } -export async function logRequestAudit( +export function logRequestAudit( data: { action: boolean; reason: number; @@ -103,12 +174,14 @@ export async function logRequestAudit( } ) { try { + // Quick synchronous check - if org has 0 retention, skip immediately if (data.orgId) { - const retentionDays = await getRetentionDays(data.orgId); - if (retentionDays == 0) { + const cached = cache.get(`org_${data.orgId}_retentionDays`); + if (cached === 0) { // do not log return; } + // If not cached or > 0, we'll log it (async retention check happens in background) } let actorType: string | undefined; @@ -128,16 +201,11 @@ export async function logRequestAudit( actorId = apiKey.apiKeyId; } - // if (!actorType || !actor || !actorId) { - // logger.warn("logRequestAudit: Incomplete actor information"); - // return; - // } - const timestamp = Math.floor(Date.now() / 1000); let metadata = null; - if (metadata) { - metadata = JSON.stringify(metadata); + if (data.metadata) { + metadata = JSON.stringify(data.metadata); } const clientIp = body.requestIp @@ -163,7 +231,8 @@ export async function logRequestAudit( })() : undefined; - await db.insert(requestAuditLog).values({ + // Add to buffer instead of writing directly to DB + auditLogBuffer.push({ timestamp, orgId: data.orgId, actorType, @@ -174,9 +243,6 @@ export async function logRequestAudit( resourceId: data.resourceId, reason: data.reason, location: data.location, - // userAgent: data.userAgent, // TODO: add this - // headers: data.body.headers, - // query: data.body.query, originalRequestURL: body.originalRequestURL, scheme: body.scheme, host: body.host, @@ -185,6 +251,26 @@ export async function logRequestAudit( ip: clientIp, tls: body.tls }); + + // Flush immediately if buffer is full, otherwise schedule a flush + if (auditLogBuffer.length >= BATCH_SIZE) { + // Fire and forget - don't block the caller + flushAuditLogs().catch((err) => + logger.error("Error flushing audit logs:", err) + ); + } else { + scheduleFlush(); + } + + // Async retention check in background (don't await) + if ( + data.orgId && + cache.get(`org_${data.orgId}_retentionDays`) === undefined + ) { + getRetentionDays(data.orgId).catch((err) => + logger.error("Error checking retention days:", err) + ); + } } catch (error) { logger.error(error); } diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index b0ad9873..7c967ace 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -1,13 +1,11 @@ -import { assertEquals } from '@test/assert'; +import { assertEquals } from "@test/assert"; function isPathAllowed(pattern: string, path: string): boolean { - // Normalize and split paths into segments const normalize = (p: string) => p.split("/").filter(Boolean); const patternParts = normalize(pattern); const pathParts = normalize(path); - // Recursive function to try different wildcard matches function matchSegments(patternIndex: number, pathIndex: number): boolean { const indent = " ".repeat(pathIndex); // Indent based on recursion depth @@ -30,7 +28,6 @@ function isPathAllowed(pattern: string, path: string): boolean { // For full segment wildcards, try consuming different numbers of path segments if (currentPatternPart === "*") { - // Try consuming 0 segments (skip the wildcard) if (matchSegments(patternIndex + 1, pathIndex)) { return true; @@ -74,69 +71,213 @@ function isPathAllowed(pattern: string, path: string): boolean { } function runTests() { - console.log('Running path matching tests...'); + console.log("Running path matching tests..."); // Test exact matching - assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed'); - assertEquals(isPathAllowed('foo', 'bar'), false, 'Different segments should not match'); - assertEquals(isPathAllowed('foo/bar', 'foo/bar'), true, 'Exact multi-segment match should be allowed'); - assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed'); + assertEquals( + isPathAllowed("foo", "foo"), + true, + "Exact match should be allowed" + ); + assertEquals( + isPathAllowed("foo", "bar"), + false, + "Different segments should not match" + ); + assertEquals( + isPathAllowed("foo/bar", "foo/bar"), + true, + "Exact multi-segment match should be allowed" + ); + assertEquals( + isPathAllowed("foo/bar", "foo/baz"), + false, + "Partial multi-segment match should not be allowed" + ); // Test with leading and trailing slashes - assertEquals(isPathAllowed('/foo', 'foo'), true, 'Pattern with leading slash should match'); - assertEquals(isPathAllowed('foo/', 'foo'), true, 'Pattern with trailing slash should match'); - assertEquals(isPathAllowed('/foo/', 'foo'), true, 'Pattern with both leading and trailing slashes should match'); - assertEquals(isPathAllowed('foo', '/foo/'), true, 'Path with leading and trailing slashes should match'); + assertEquals( + isPathAllowed("/foo", "foo"), + true, + "Pattern with leading slash should match" + ); + assertEquals( + isPathAllowed("foo/", "foo"), + true, + "Pattern with trailing slash should match" + ); + assertEquals( + isPathAllowed("/foo/", "foo"), + true, + "Pattern with both leading and trailing slashes should match" + ); + assertEquals( + isPathAllowed("foo", "/foo/"), + true, + "Path with leading and trailing slashes should match" + ); // Test simple wildcard matching - assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment'); - assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments'); - assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match'); - assertEquals(isPathAllowed('foo/*', 'foo/bar'), true, 'Wildcard suffix should match'); - assertEquals(isPathAllowed('foo/*/baz', 'foo/bar/baz'), true, 'Wildcard in middle should match'); + assertEquals( + isPathAllowed("*", "foo"), + true, + "Single wildcard should match any single segment" + ); + assertEquals( + isPathAllowed("*", "foo/bar"), + true, + "Single wildcard should match multiple segments" + ); + assertEquals( + isPathAllowed("*/bar", "foo/bar"), + true, + "Wildcard prefix should match" + ); + assertEquals( + isPathAllowed("foo/*", "foo/bar"), + true, + "Wildcard suffix should match" + ); + assertEquals( + isPathAllowed("foo/*/baz", "foo/bar/baz"), + true, + "Wildcard in middle should match" + ); // Test multiple wildcards - assertEquals(isPathAllowed('*/*', 'foo/bar'), true, 'Multiple wildcards should match corresponding segments'); - assertEquals(isPathAllowed('*/*/*', 'foo/bar/baz'), true, 'Three wildcards should match three segments'); - assertEquals(isPathAllowed('foo/*/*', 'foo/bar/baz'), true, 'Specific prefix with wildcards should match'); - assertEquals(isPathAllowed('*/*/baz', 'foo/bar/baz'), true, 'Wildcards with specific suffix should match'); + assertEquals( + isPathAllowed("*/*", "foo/bar"), + true, + "Multiple wildcards should match corresponding segments" + ); + assertEquals( + isPathAllowed("*/*/*", "foo/bar/baz"), + true, + "Three wildcards should match three segments" + ); + assertEquals( + isPathAllowed("foo/*/*", "foo/bar/baz"), + true, + "Specific prefix with wildcards should match" + ); + assertEquals( + isPathAllowed("*/*/baz", "foo/bar/baz"), + true, + "Wildcards with specific suffix should match" + ); // Test wildcard consumption behavior - assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments'); - assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional'); - assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments'); - assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped'); + assertEquals( + isPathAllowed("*", ""), + true, + "Wildcard should optionally consume segments" + ); + assertEquals( + isPathAllowed("foo/*", "foo"), + true, + "Trailing wildcard should be optional" + ); + assertEquals( + isPathAllowed("*/*", "foo"), + true, + "Multiple wildcards can match fewer segments" + ); + assertEquals( + isPathAllowed("*/*/*", "foo/bar"), + true, + "Extra wildcards can be skipped" + ); // Test complex nested paths - assertEquals(isPathAllowed('api/*/users', 'api/v1/users'), true, 'API versioning pattern should match'); - assertEquals(isPathAllowed('api/*/users/*', 'api/v1/users/123'), true, 'API resource pattern should match'); - assertEquals(isPathAllowed('api/*/users/*/profile', 'api/v1/users/123/profile'), true, 'Nested API pattern should match'); + assertEquals( + isPathAllowed("api/*/users", "api/v1/users"), + true, + "API versioning pattern should match" + ); + assertEquals( + isPathAllowed("api/*/users/*", "api/v1/users/123"), + true, + "API resource pattern should match" + ); + assertEquals( + isPathAllowed("api/*/users/*/profile", "api/v1/users/123/profile"), + true, + "Nested API pattern should match" + ); // Test for the requested padbootstrap* pattern - assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap'); - assertEquals(isPathAllowed('padbootstrap*', 'padbootstrapv1'), true, 'padbootstrap* should match padbootstrapv1'); - assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap/files'), false, 'padbootstrap* should not match padbootstrap/files'); - assertEquals(isPathAllowed('padbootstrap*/*', 'padbootstrap/files'), true, 'padbootstrap*/* should match padbootstrap/files'); - assertEquals(isPathAllowed('padbootstrap*/files', 'padbootstrapv1/files'), true, 'padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)'); + assertEquals( + isPathAllowed("padbootstrap*", "padbootstrap"), + true, + "padbootstrap* should match padbootstrap" + ); + assertEquals( + isPathAllowed("padbootstrap*", "padbootstrapv1"), + true, + "padbootstrap* should match padbootstrapv1" + ); + assertEquals( + isPathAllowed("padbootstrap*", "padbootstrap/files"), + false, + "padbootstrap* should not match padbootstrap/files" + ); + assertEquals( + isPathAllowed("padbootstrap*/*", "padbootstrap/files"), + true, + "padbootstrap*/* should match padbootstrap/files" + ); + assertEquals( + isPathAllowed("padbootstrap*/files", "padbootstrapv1/files"), + true, + "padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)" + ); // Test wildcard edge cases - assertEquals(isPathAllowed('*/*/*/*/*/*', 'a/b'), true, 'Many wildcards can match few segments'); - assertEquals(isPathAllowed('a/*/b/*/c', 'a/anything/b/something/c'), true, 'Multiple wildcards in pattern should match corresponding segments'); + assertEquals( + isPathAllowed("*/*/*/*/*/*", "a/b"), + true, + "Many wildcards can match few segments" + ); + assertEquals( + isPathAllowed("a/*/b/*/c", "a/anything/b/something/c"), + true, + "Multiple wildcards in pattern should match corresponding segments" + ); // Test patterns with partial segment matches - assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap-123'), true, 'Wildcards in isPathAllowed should be segment-based, not character-based'); - assertEquals(isPathAllowed('test*', 'testuser'), true, 'Asterisk as part of segment name is treated as a literal, not a wildcard'); - assertEquals(isPathAllowed('my*app', 'myapp'), true, 'Asterisk in middle of segment name is treated as a literal, not a wildcard'); + assertEquals( + isPathAllowed("padbootstrap*", "padbootstrap-123"), + true, + "Wildcards in isPathAllowed should be segment-based, not character-based" + ); + assertEquals( + isPathAllowed("test*", "testuser"), + true, + "Asterisk as part of segment name is treated as a literal, not a wildcard" + ); + assertEquals( + isPathAllowed("my*app", "myapp"), + true, + "Asterisk in middle of segment name is treated as a literal, not a wildcard" + ); - assertEquals(isPathAllowed('/', '/'), true, 'Root path should match root path'); - assertEquals(isPathAllowed('/', '/test'), false, 'Root path should not match non-root path'); + assertEquals( + isPathAllowed("/", "/"), + true, + "Root path should match root path" + ); + assertEquals( + isPathAllowed("/", "/test"), + false, + "Root path should not match non-root path" + ); - console.log('All tests passed!'); + console.log("All tests passed!"); } // Run all tests try { runTests(); } catch (error) { - console.error('Test failed:', error); + console.error("Test failed:", error); } diff --git a/server/routers/billing/types.ts b/server/routers/billing/types.ts index 2ec5a1b1..4e0aab52 100644 --- a/server/routers/billing/types.ts +++ b/server/routers/billing/types.ts @@ -14,4 +14,3 @@ export type GetOrgTierResponse = { tier: string | null; active: boolean; }; - diff --git a/server/routers/billing/webhooks.ts b/server/routers/billing/webhooks.ts index 0ca38a8a..53eda78c 100644 --- a/server/routers/billing/webhooks.ts +++ b/server/routers/billing/webhooks.ts @@ -11,4 +11,4 @@ export async function billingWebhookHandler( return next( createHttpError(HttpCode.NOT_FOUND, "This endpoint is not in use") ); -} \ No newline at end of file +} diff --git a/server/routers/blueprints/applyJSONBlueprint.ts b/server/routers/blueprints/applyJSONBlueprint.ts index f8c9caec..7eee15bf 100644 --- a/server/routers/blueprints/applyJSONBlueprint.ts +++ b/server/routers/blueprints/applyJSONBlueprint.ts @@ -9,12 +9,12 @@ import { OpenAPITags, registry } from "@server/openApi"; import { applyBlueprint } from "@server/lib/blueprints/applyBlueprint"; const applyBlueprintSchema = z.strictObject({ - blueprint: z.string() - }); + blueprint: z.string() +}); const applyBlueprintParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); registry.registerPath({ method: "put", diff --git a/server/routers/blueprints/getBlueprint.ts b/server/routers/blueprints/getBlueprint.ts index 45c36af7..915e0481 100644 --- a/server/routers/blueprints/getBlueprint.ts +++ b/server/routers/blueprints/getBlueprint.ts @@ -13,12 +13,9 @@ import { OpenAPITags, registry } from "@server/openApi"; import { BlueprintData } from "./types"; const getBlueprintSchema = z.strictObject({ - blueprintId: z - .string() - .transform(stoi) - .pipe(z.int().positive()), - orgId: z.string() - }); + blueprintId: z.string().transform(stoi).pipe(z.int().positive()), + orgId: z.string() +}); async function query(blueprintId: number, orgId: string) { // Get the client diff --git a/server/routers/blueprints/listBlueprints.ts b/server/routers/blueprints/listBlueprints.ts index 315abfed..2ece9e53 100644 --- a/server/routers/blueprints/listBlueprints.ts +++ b/server/routers/blueprints/listBlueprints.ts @@ -11,23 +11,23 @@ import { OpenAPITags, registry } from "@server/openApi"; import { BlueprintData } from "./types"; const listBluePrintsParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listBluePrintsSchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function queryBlueprints(orgId: string, limit: number, offset: number) { const res = await db diff --git a/server/routers/certificates/createCertificate.ts b/server/routers/certificates/createCertificate.ts index e160e644..e858e5cd 100644 --- a/server/routers/certificates/createCertificate.ts +++ b/server/routers/certificates/createCertificate.ts @@ -1,5 +1,9 @@ import { db, Transaction } from "@server/db"; -export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) { +export async function createCertificate( + domainId: string, + domain: string, + trx: Transaction | typeof db +) { return; -} \ No newline at end of file +} diff --git a/server/routers/certificates/types.ts b/server/routers/certificates/types.ts index 80136de8..3ec90857 100644 --- a/server/routers/certificates/types.ts +++ b/server/routers/certificates/types.ts @@ -10,4 +10,4 @@ export type GetCertificateResponse = { updatedAt: string; errorMessage?: string | null; renewalCount: number; -} \ No newline at end of file +}; diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 160006e1..ea3a371d 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -25,6 +25,7 @@ import { listExitNodes } from "#dynamic/lib/exitNodes"; import { generateId } from "@server/auth/sessions/app"; import { OpenAPITags, registry } from "@server/openApi"; import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations"; +import { getUniqueClientName } from "@server/db/names"; const createClientParamsSchema = z.strictObject({ orgId: z.string() @@ -206,9 +207,12 @@ export async function createClient( ); } + const niceId = await getUniqueClientName(orgId); + [newClient] = await trx .insert(clients) .values({ + niceId, exitNodeId: randomExitNode.exitNodeId, orgId, name, diff --git a/server/routers/client/createUserClient.ts b/server/routers/client/createUserClient.ts index e5b5ea8f..5e9840f9 100644 --- a/server/routers/client/createUserClient.ts +++ b/server/routers/client/createUserClient.ts @@ -22,6 +22,7 @@ import { isIpInCidr } from "@server/lib/ip"; import { listExitNodes } from "#dynamic/lib/exitNodes"; import { OpenAPITags, registry } from "@server/openApi"; import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations"; +import { getUniqueClientName } from "@server/db/names"; const paramsSchema = z .object({ @@ -211,11 +212,14 @@ export async function createUserClient( ); } + const niceId = await getUniqueClientName(orgId); + [newClient] = await trx .insert(clients) .values({ exitNodeId: randomExitNode.exitNodeId, orgId, + niceId, name, subnet: updatedSubnet, type, diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index c9d22b10..cfb2652b 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; -import { clients, clientSitesAssociationsCache } from "@server/db"; +import { db, olms } from "@server/db"; +import { clients } from "@server/db"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -12,35 +12,56 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const getClientSchema = z.strictObject({ - clientId: z.string().transform(stoi).pipe(z.int().positive()) - }); + clientId: z + .string() + .optional() + .transform(stoi) + .pipe(z.int().positive().optional()) + .optional(), + niceId: z.string().optional(), + orgId: z.string().optional() +}); -async function query(clientId: number) { - // Get the client - const [client] = await db - .select() - .from(clients) - .where(and(eq(clients.clientId, clientId))) - .limit(1); - - if (!client) { - return null; +async function query(clientId?: number, niceId?: string, orgId?: string) { + if (clientId) { + const [res] = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .leftJoin(olms, eq(clients.clientId, olms.clientId)) + .limit(1); + return res; + } else if (niceId && orgId) { + const [res] = await db + .select() + .from(clients) + .where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId))) + .leftJoin(olms, eq(olms.clientId, olms.clientId)) + .limit(1); + return res; } - - // Get the siteIds associated with this client - const sites = await db - .select({ siteId: clientSitesAssociationsCache.siteId }) - .from(clientSitesAssociationsCache) - .where(eq(clientSitesAssociationsCache.clientId, clientId)); - - // Add the siteIds to the client object - return { - ...client, - siteIds: sites.map((site) => site.siteId) - }; } -export type GetClientResponse = NonNullable>>; +export type GetClientResponse = NonNullable< + Awaited> +>["clients"] & { + olmId: string | null; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/client/{niceId}", + description: + "Get a client by orgId and niceId. NiceId is a readable ID for the site and unique on a per org basis.", + tags: [OpenAPITags.Org, OpenAPITags.Site], + request: { + params: z.object({ + orgId: z.string(), + niceId: z.string() + }) + }, + responses: {} +}); registry.registerPath({ method: "get", @@ -48,7 +69,9 @@ registry.registerPath({ description: "Get a client by its client ID.", tags: [OpenAPITags.Client], request: { - params: getClientSchema + params: z.object({ + clientId: z.number() + }) }, responses: {} }); @@ -72,9 +95,9 @@ export async function getClient( ); } - const { clientId } = parsedParams.data; + const { clientId, niceId, orgId } = parsedParams.data; - const client = await query(clientId); + const client = await query(clientId, niceId, orgId); if (!client) { return next( @@ -82,8 +105,13 @@ export async function getClient( ); } + const data: GetClientResponse = { + ...client.clients, + olmId: client.olms ? client.olms.olmId : null + }; + return response(res, { - data: client, + data, success: true, error: false, message: "Client retrieved successfully", diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index ff1050ef..42e47efe 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -10,7 +10,16 @@ import { import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; -import { and, count, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm"; +import { + and, + count, + eq, + inArray, + isNotNull, + isNull, + or, + sql +} from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -60,13 +69,9 @@ async function getLatestOlmVersion(): Promise { return latestVersion; } catch (error: any) { if (error.name === "AbortError") { - logger.warn( - "Request to fetch latest Olm version timed out (1.5s)" - ); + logger.warn("Request to fetch latest Olm version timed out (1.5s)"); } else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { - logger.warn( - "Connection timeout while fetching latest Olm version" - ); + logger.warn("Connection timeout while fetching latest Olm version"); } else { logger.warn( "Error fetching latest Olm version:", @@ -77,10 +82,9 @@ async function getLatestOlmVersion(): Promise { } } - const listClientsParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listClientsSchema = z.object({ limit: z @@ -95,12 +99,14 @@ const listClientsSchema = z.object({ .default("0") .transform(Number) .pipe(z.int().nonnegative()), - filter: z - .enum(["user", "machine"]) - .optional() + filter: z.enum(["user", "machine"]).optional() }); -function queryClients(orgId: string, accessibleClientIds: number[], filter?: "user" | "machine") { +function queryClients( + orgId: string, + accessibleClientIds: number[], + filter?: "user" | "machine" +) { const conditions = [ inArray(clients.clientId, accessibleClientIds), eq(clients.orgId, orgId) @@ -128,7 +134,9 @@ function queryClients(orgId: string, accessibleClientIds: number[], filter?: "us olmVersion: olms.version, userId: clients.userId, username: users.username, - userEmail: users.email + userEmail: users.email, + niceId: clients.niceId, + agent: olms.agent }) .from(clients) .leftJoin(orgs, eq(clients.orgId, orgs.orgId)) @@ -156,16 +164,17 @@ type OlmWithUpdateAvailable = Awaited>[0] & { olmUpdateAvailable?: boolean; }; - export type ListClientsResponse = { - clients: Array>[0] & { - sites: Array<{ - siteId: number; - siteName: string | null; - siteNiceId: string | null; - }> - olmUpdateAvailable?: boolean; - }>; + clients: Array< + Awaited>[0] & { + sites: Array<{ + siteId: number; + siteName: string | null; + siteNiceId: string | null; + }>; + olmUpdateAvailable?: boolean; + } + >; pagination: { total: number; limit: number; offset: number }; }; @@ -269,28 +278,34 @@ export async function listClients( const totalCount = totalCountResult[0].count; // Get associated sites for all clients - const clientIds = clientsList.map(client => client.clientId); + const clientIds = clientsList.map((client) => client.clientId); const siteAssociations = await getSiteAssociations(clientIds); // Group site associations by client ID - const sitesByClient = siteAssociations.reduce((acc, association) => { - if (!acc[association.clientId]) { - acc[association.clientId] = []; - } - acc[association.clientId].push({ - siteId: association.siteId, - siteName: association.siteName, - siteNiceId: association.siteNiceId - }); - return acc; - }, {} as Record>); + const sitesByClient = siteAssociations.reduce( + (acc, association) => { + if (!acc[association.clientId]) { + acc[association.clientId] = []; + } + acc[association.clientId].push({ + siteId: association.siteId, + siteName: association.siteName, + siteNiceId: association.siteNiceId + }); + return acc; + }, + {} as Record< + number, + Array<{ + siteId: number; + siteName: string | null; + siteNiceId: string | null; + }> + > + ); // Merge clients with their site associations - const clientsWithSites = clientsList.map(client => ({ + const clientsWithSites = clientsList.map((client) => ({ ...client, sites: sitesByClient[client.clientId] || [] })); @@ -320,7 +335,6 @@ export async function listClients( } catch (error) { client.olmUpdateAvailable = false; } - }); } } catch (error) { @@ -331,7 +345,6 @@ export async function listClients( ); } - return response(res, { data: { clients: clientsWithSites, diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index 3d447ecd..fd31da12 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -16,8 +16,8 @@ export type PickClientDefaultsResponse = { }; const pickClientDefaultsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); registry.registerPath({ method: "get", diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index c9bb910b..b7b91925 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -1,5 +1,5 @@ import { sendToClient } from "#dynamic/routers/ws"; -import { db, olms } from "@server/db"; +import { db, olms, Transaction } from "@server/db"; import { Alias, SubnetProxyTarget } from "@server/lib/ip"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; @@ -101,14 +101,18 @@ export async function removePeerData( export async function updatePeerData( clientId: number, siteId: number, - remoteSubnets: { - oldRemoteSubnets: string[]; - newRemoteSubnets: string[]; - } | undefined, - aliases: { - oldAliases: Alias[]; - newAliases: Alias[]; - } | undefined, + remoteSubnets: + | { + oldRemoteSubnets: string[]; + newRemoteSubnets: string[]; + } + | undefined, + aliases: + | { + oldAliases: Alias[]; + newAliases: Alias[]; + } + | undefined, olmId?: string ) { if (!olmId) { diff --git a/server/routers/client/terminate.ts b/server/routers/client/terminate.ts index dc49ef05..1cfdc709 100644 --- a/server/routers/client/terminate.ts +++ b/server/routers/client/terminate.ts @@ -2,7 +2,10 @@ import { sendToClient } from "#dynamic/routers/ws"; import { db, olms } from "@server/db"; import { eq } from "drizzle-orm"; -export async function sendTerminateClient(clientId: number, olmId?: string | null) { +export async function sendTerminateClient( + clientId: number, + olmId?: string | null +) { if (!olmId) { const [olm] = await db .select() diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index e2f11a7c..12d0a199 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { Client, db, exitNodes, olms, sites } from "@server/db"; -import { clients, clientSitesAssociationsCache } from "@server/db"; +import { db } from "@server/db"; +import { clients } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -15,7 +15,8 @@ const updateClientParamsSchema = z.strictObject({ }); const updateClientSchema = z.strictObject({ - name: z.string().min(1).max(255).optional() + name: z.string().min(1).max(255).optional(), + niceId: z.string().min(1).max(255).optional() }); export type UpdateClientBody = z.infer; @@ -54,7 +55,7 @@ export async function updateClient( ); } - const { name } = parsedBody.data; + const { name, niceId } = parsedBody.data; const parsedParams = updateClientParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -84,9 +85,32 @@ export async function updateClient( ); } + // if niceId is provided, check if it's already in use by another client + if (niceId) { + const [existingClient] = await db + .select() + .from(clients) + .where( + and( + eq(clients.niceId, niceId), + eq(clients.orgId, clients.orgId) + ) + ) + .limit(1); + + if (existingClient) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A client with niceId "${niceId}" already exists` + ) + ); + } + } + const updatedClient = await db .update(clients) - .set({ name }) + .set({ name, niceId }) .where(eq(clients.clientId, clientId)) .returning(); diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 3f223bce..6558d748 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -1,6 +1,13 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, Domain, domains, OrgDomains, orgDomains, dnsRecords } from "@server/db"; +import { + db, + Domain, + domains, + OrgDomains, + orgDomains, + dnsRecords +} from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -16,16 +23,15 @@ import { build } from "@server/build"; import config from "@server/lib/config"; const paramsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const bodySchema = z.strictObject({ - type: z.enum(["ns", "cname", "wildcard"]), - baseDomain: subdomainSchema, - certResolver: z.string().optional().nullable(), - preferWildcardCert: z.boolean().optional().nullable() // optional, only for wildcard - }); - + type: z.enum(["ns", "cname", "wildcard"]), + baseDomain: subdomainSchema, + certResolver: z.string().optional().nullable(), + preferWildcardCert: z.boolean().optional().nullable() // optional, only for wildcard +}); export type CreateDomainResponse = { domainId: string; @@ -72,7 +78,8 @@ export async function createOrgDomain( } const { orgId } = parsedParams.data; - const { type, baseDomain, certResolver, preferWildcardCert } = parsedBody.data; + const { type, baseDomain, certResolver, preferWildcardCert } = + parsedBody.data; if (build == "oss") { if (type !== "wildcard") { @@ -278,7 +285,7 @@ export async function createOrgDomain( // TODO: This needs to be cross region and not hardcoded if (type === "ns") { nsRecords = config.getRawConfig().dns.nameservers as string[]; - + // Save NS records to database for (const nsValue of nsRecords) { recordsToInsert.push({ @@ -300,7 +307,7 @@ export async function createOrgDomain( baseDomain: `_acme-challenge.${baseDomain}` } ]; - + // Save CNAME records to database for (const cnameRecord of cnameRecords) { recordsToInsert.push({ @@ -322,7 +329,7 @@ export async function createOrgDomain( baseDomain: `${baseDomain}` } ]; - + // Save A records to database for (const aRecord of aRecords) { recordsToInsert.push({ diff --git a/server/routers/domain/deleteOrgDomain.ts b/server/routers/domain/deleteOrgDomain.ts index fe4a4805..fa916beb 100644 --- a/server/routers/domain/deleteOrgDomain.ts +++ b/server/routers/domain/deleteOrgDomain.ts @@ -11,9 +11,9 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; const paramsSchema = z.strictObject({ - domainId: z.string(), - orgId: z.string() - }); + domainId: z.string(), + orgId: z.string() +}); export type DeleteAccountDomainResponse = { success: boolean; @@ -48,10 +48,7 @@ export async function deleteAccountDomain( eq(orgDomains.domainId, domainId) ) ) - .innerJoin( - domains, - eq(orgDomains.domainId, domains.domainId) - ); + .innerJoin(domains, eq(orgDomains.domainId, domains.domainId)); if (!existing) { return next( diff --git a/server/routers/domain/getDNSRecords.ts b/server/routers/domain/getDNSRecords.ts index 239cc455..5a373a11 100644 --- a/server/routers/domain/getDNSRecords.ts +++ b/server/routers/domain/getDNSRecords.ts @@ -11,16 +11,16 @@ import { OpenAPITags, registry } from "@server/openApi"; import { getServerIp } from "@server/lib/serverIpService"; // your in-memory IP module const getDNSRecordsSchema = z.strictObject({ - domainId: z.string(), - orgId: z.string() - }); + domainId: z.string(), + orgId: z.string() +}); async function query(domainId: string) { const records = await db .select() .from(dnsRecords) .where(eq(dnsRecords.domainId, domainId)); - + return records; } @@ -72,8 +72,11 @@ export async function getDNSRecords( const serverIp = getServerIp(); // Override value for type A or wildcard records - const updatedRecords = records.map(record => { - if ((record.recordType === "A" || record.baseDomain === "*") && serverIp) { + const updatedRecords = records.map((record) => { + if ( + (record.recordType === "A" || record.baseDomain === "*") && + serverIp + ) { return { ...record, value: serverIp }; } return record; @@ -92,4 +95,4 @@ export async function getDNSRecords( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/routers/domain/getDomain.ts b/server/routers/domain/getDomain.ts index 408cf37d..3e5565f9 100644 --- a/server/routers/domain/getDomain.ts +++ b/server/routers/domain/getDomain.ts @@ -11,11 +11,9 @@ import { OpenAPITags, registry } from "@server/openApi"; import { domain } from "zod/v4/core/regexes"; const getDomainSchema = z.strictObject({ - domainId: z - .string() - .optional(), - orgId: z.string().optional() - }); + domainId: z.string().optional(), + orgId: z.string().optional() +}); async function query(domainId?: string, orgId?: string) { if (domainId) { @@ -65,7 +63,9 @@ export async function getDomain( const domain = await query(domainId, orgId); if (!domain) { - return next(createHttpError(HttpCode.NOT_FOUND, "Domain not found")); + return next( + createHttpError(HttpCode.NOT_FOUND, "Domain not found") + ); } return response(res, { diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts index e7e0b555..73b28fea 100644 --- a/server/routers/domain/index.ts +++ b/server/routers/domain/index.ts @@ -4,4 +4,4 @@ export * from "./deleteOrgDomain"; export * from "./restartOrgDomain"; export * from "./getDomain"; export * from "./getDNSRecords"; -export * from "./updateDomain"; \ No newline at end of file +export * from "./updateDomain"; diff --git a/server/routers/domain/listDomains.ts b/server/routers/domain/listDomains.ts index 48f22c6c..20b23634 100644 --- a/server/routers/domain/listDomains.ts +++ b/server/routers/domain/listDomains.ts @@ -11,23 +11,23 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const listDomainsParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listDomainsSchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function queryDomains(orgId: string, limit: number, offset: number) { const res = await db diff --git a/server/routers/domain/restartOrgDomain.ts b/server/routers/domain/restartOrgDomain.ts index f2bf7c39..1039d2fb 100644 --- a/server/routers/domain/restartOrgDomain.ts +++ b/server/routers/domain/restartOrgDomain.ts @@ -9,9 +9,9 @@ import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; const paramsSchema = z.strictObject({ - domainId: z.string(), - orgId: z.string() - }); + domainId: z.string(), + orgId: z.string() +}); export type RestartOrgDomainResponse = { success: boolean; diff --git a/server/routers/domain/types.ts b/server/routers/domain/types.ts index 4ae48fb1..ececc2db 100644 --- a/server/routers/domain/types.ts +++ b/server/routers/domain/types.ts @@ -5,4 +5,4 @@ export type CheckDomainAvailabilityResponse = { domainId: string; fullDomain: string; }[]; -}; \ No newline at end of file +}; diff --git a/server/routers/domain/updateDomain.ts b/server/routers/domain/updateDomain.ts index 08301189..64e78641 100644 --- a/server/routers/domain/updateDomain.ts +++ b/server/routers/domain/updateDomain.ts @@ -10,14 +10,14 @@ import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.strictObject({ - orgId: z.string(), - domainId: z.string() - }); + orgId: z.string(), + domainId: z.string() +}); const bodySchema = z.strictObject({ - certResolver: z.string().optional().nullable(), - preferWildcardCert: z.boolean().optional().nullable() - }); + certResolver: z.string().optional().nullable(), + preferWildcardCert: z.boolean().optional().nullable() +}); export type UpdateDomainResponse = { domainId: string; @@ -25,7 +25,6 @@ export type UpdateDomainResponse = { preferWildcardCert: boolean | null; }; - registry.registerPath({ method: "patch", path: "/org/{orgId}/domain/{domainId}", @@ -88,7 +87,6 @@ export async function updateOrgDomain( ); } - const [existingDomain] = await db .select() .from(domains) @@ -154,4 +152,4 @@ export async function updateOrgDomain( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/routers/external.ts b/server/routers/external.ts index 188654bc..54b48c6e 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -111,6 +111,7 @@ authenticated.get( authenticated.get( "/org/:orgId/site/:niceId", verifyOrgAccess, + verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), site.getSite ); @@ -149,6 +150,14 @@ authenticated.get( client.getClient ); +authenticated.get( + "/org/:orgId/client/:niceId", + verifyOrgAccess, + verifyClientAccess, + verifyUserHasAction(ActionsEnum.getClient), + client.getClient +); + authenticated.put( "/org/:orgId/client", verifyOrgAccess, @@ -309,7 +318,7 @@ authenticated.post( verifyRoleAccess, verifyUserHasAction(ActionsEnum.setResourceRoles), logActionAudit(ActionsEnum.setResourceRoles), - siteResource.setSiteResourceRoles, + siteResource.setSiteResourceRoles ); authenticated.post( @@ -318,7 +327,7 @@ authenticated.post( verifySetResourceUsers, verifyUserHasAction(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers), - siteResource.setSiteResourceUsers, + siteResource.setSiteResourceUsers ); authenticated.post( @@ -327,7 +336,7 @@ authenticated.post( verifySetResourceClients, verifyUserHasAction(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers), - siteResource.setSiteResourceClients, + siteResource.setSiteResourceClients ); authenticated.post( @@ -336,7 +345,7 @@ authenticated.post( verifySetResourceClients, verifyUserHasAction(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers), - siteResource.addClientToSiteResource, + siteResource.addClientToSiteResource ); authenticated.post( @@ -345,7 +354,7 @@ authenticated.post( verifySetResourceClients, verifyUserHasAction(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers), - siteResource.removeClientFromSiteResource, + siteResource.removeClientFromSiteResource ); authenticated.put( @@ -458,6 +467,7 @@ authenticated.get( authenticated.get( "/org/:orgId/resource/:niceId", verifyOrgAccess, + verifyResourceAccess, verifyUserHasAction(ActionsEnum.getResource), resource.getResource ); @@ -802,17 +812,9 @@ authenticated.delete( // createNewt // ); -authenticated.put( - "/user/:userId/olm", - verifyIsLoggedInUser, - olm.createUserOlm -); +authenticated.put("/user/:userId/olm", verifyIsLoggedInUser, olm.createUserOlm); -authenticated.get( - "/user/:userId/olms", - verifyIsLoggedInUser, - olm.listUserOlms -); +authenticated.get("/user/:userId/olms", verifyIsLoggedInUser, olm.listUserOlms); authenticated.delete( "/user/:userId/olm/:olmId", diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts index 4c5efed7..76e86265 100644 --- a/server/routers/generatedLicense/types.ts +++ b/server/routers/generatedLicense/types.ts @@ -27,4 +27,4 @@ export type NewLicenseKey = { }; }; -export type GenerateNewLicenseResponse = NewLicenseKey; \ No newline at end of file +export type GenerateNewLicenseResponse = NewLicenseKey; diff --git a/server/routers/gerbil/createExitNode.ts b/server/routers/gerbil/createExitNode.ts index 8148ed75..bc965036 100644 --- a/server/routers/gerbil/createExitNode.ts +++ b/server/routers/gerbil/createExitNode.ts @@ -5,7 +5,10 @@ import { getNextAvailableSubnet } from "@server/lib/exitNodes"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; -export async function createExitNode(publicKey: string, reachableAt: string | undefined) { +export async function createExitNode( + publicKey: string, + reachableAt: string | undefined +) { // Fetch exit node const [exitNodeQuery] = await db.select().from(exitNodes).limit(1); let exitNode: ExitNode; diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 56ebd744..ba3ab7ad 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -117,4 +117,4 @@ export async function generateGerbilConfig(exitNode: ExitNode) { }; return configResponse; -} \ No newline at end of file +} diff --git a/server/routers/gerbil/index.ts b/server/routers/gerbil/index.ts index bff57d05..aa957d3a 100644 --- a/server/routers/gerbil/index.ts +++ b/server/routers/gerbil/index.ts @@ -2,4 +2,4 @@ export * from "./getConfig"; export * from "./receiveBandwidth"; export * from "./updateHolePunch"; export * from "./getAllRelays"; -export * from "./getResolvedHostname"; \ No newline at end of file +export * from "./getResolvedHostname"; diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index ffbd05c1..5c9cacb2 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -14,12 +14,55 @@ import { build } from "@server/build"; // Track sites that are already offline to avoid unnecessary queries const offlineSites = new Set(); +// Retry configuration for deadlock handling +const MAX_RETRIES = 3; +const BASE_DELAY_MS = 50; + interface PeerBandwidth { publicKey: string; bytesIn: number; bytesOut: number; } +/** + * Check if an error is a deadlock error + */ +function isDeadlockError(error: any): boolean { + return ( + error?.code === "40P01" || + error?.cause?.code === "40P01" || + (error?.message && error.message.includes("deadlock")) + ); +} + +/** + * Execute a function with retry logic for deadlock handling + */ +async function withDeadlockRetry( + operation: () => Promise, + context: string +): Promise { + let attempt = 0; + while (true) { + try { + return await operation(); + } catch (error: any) { + if (isDeadlockError(error) && attempt < MAX_RETRIES) { + attempt++; + const baseDelay = Math.pow(2, attempt - 1) * BASE_DELAY_MS; + const jitter = Math.random() * baseDelay; + const delay = baseDelay + jitter; + logger.warn( + `Deadlock detected in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms` + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + throw error; + } + } +} + export const receiveBandwidth = async ( req: Request, res: Response, @@ -60,201 +103,215 @@ export async function updateSiteBandwidth( const currentTime = new Date(); const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago - // logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`); + // Sort bandwidth data by publicKey to ensure consistent lock ordering across all instances + // This is critical for preventing deadlocks when multiple instances update the same sites + const sortedBandwidthData = [...bandwidthData].sort((a, b) => + a.publicKey.localeCompare(b.publicKey) + ); - 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 + // First, handle sites that are actively reporting bandwidth + const activePeers = sortedBandwidthData.filter((peer) => peer.bytesIn > 0); - 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 (collected outside transaction) + const orgUsageMap = new Map(); + const orgUptimeMap = new Map(); - // Aggregate usage data by organization - const orgUsageMap = new Map(); - const orgUptimeMap = new Map(); + if (activePeers.length > 0) { + // Remove any active peers from offline tracking since they're sending data + activePeers.forEach((peer) => offlineSites.delete(peer.publicKey)); - // 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 - }); + // Update each active site individually with retry logic + // This reduces transaction scope and allows retries per-site + for (const peer of activePeers) { + try { + const updatedSite = await withDeadlockRetry(async () => { + const [result] = await db + .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 + }); + return result; + }, `update active site ${peer.publicKey}`); if (updatedSite) { if (exitNodeId) { - if ( - await checkExitNodeOrg( - exitNodeId, - updatedSite.orgId, - trx - ) - ) { - // not allowed + const notAllowed = await checkExitNodeOrg( + exitNodeId, + updatedSite.orgId + ); + if (notAllowed) { 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"); + // Skip this site but continue processing others + continue; } } - updatedSites.push({ ...updatedSite, 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 - } - - if (calcUsageAndLimits) { - // REMOTE EXIT NODES DO NOT COUNT TOWARDS USAGE - // Process all usage updates sequentially by organization to reduce deadlock risk - const allOrgIds = new Set([...orgUsageMap.keys(), ...orgUptimeMap.keys()]); - - for (const orgId of allOrgIds) { - try { - // Process bandwidth usage for this org - const totalBandwidth = orgUsageMap.get(orgId); - if (totalBandwidth) { - const bandwidthUsage = await usageService.add( - orgId, - FeatureId.EGRESS_DATA_MB, - totalBandwidth, - trx - ); - if (bandwidthUsage) { - usageService - .checkLimitSet( - orgId, - true, - FeatureId.EGRESS_DATA_MB, - bandwidthUsage, - trx - ) - .catch((error: any) => { - logger.error( - `Error checking bandwidth limits for org ${orgId}:`, - error - ); - }); - } - } - - // Process uptime usage for this org - const totalUptime = orgUptimeMap.get(orgId); - if (totalUptime) { - const uptimeUsage = await usageService.add( - orgId, - FeatureId.SITE_UPTIME, - totalUptime, - trx - ); - if (uptimeUsage) { - usageService - .checkLimitSet( - orgId, - true, - FeatureId.SITE_UPTIME, - uptimeUsage, - trx - ) - .catch((error: any) => { - logger.error( - `Error checking uptime limits for org ${orgId}:`, - error - ); - }); - } - } - } catch (error) { - logger.error( - `Error processing usage for org ${orgId}:`, - error - ); - // Don't break the loop, continue with other orgs - } + // Aggregate bandwidth usage for the org + const totalBandwidth = peer.bytesIn + peer.bytesOut; + const currentOrgUsage = + orgUsageMap.get(updatedSite.orgId) || 0; + orgUsageMap.set( + updatedSite.orgId, + currentOrgUsage + totalBandwidth + ); + + // Add 10 seconds of uptime for each active site + const currentOrgUptime = + orgUptimeMap.get(updatedSite.orgId) || 0; + orgUptimeMap.set( + updatedSite.orgId, + currentOrgUptime + 10 / 60 + ); } + } catch (error) { + logger.error( + `Failed to update bandwidth for site ${peer.publicKey}:`, + error + ); + // Continue with other sites } } + } - // 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 - ); + // Process usage updates outside of site update transactions + // This separates the concerns and reduces lock contention + if (calcUsageAndLimits && (orgUsageMap.size > 0 || orgUptimeMap.size > 0)) { + // Sort org IDs to ensure consistent lock ordering + const allOrgIds = [ + ...new Set([...orgUsageMap.keys(), ...orgUptimeMap.keys()]) + ].sort(); - 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 + for (const orgId of allOrgIds) { + try { + // Process bandwidth usage for this org + const totalBandwidth = orgUsageMap.get(orgId); + if (totalBandwidth) { + const bandwidthUsage = await usageService.add( + orgId, + FeatureId.EGRESS_DATA_MB, + totalBandwidth ); - if (lastUpdateWithData < oneMinuteAgo) { - newOnlineStatus = false; + if (bandwidthUsage) { + // Fire and forget - don't block on limit checking + usageService + .checkLimitSet( + orgId, + true, + FeatureId.EGRESS_DATA_MB, + bandwidthUsage + ) + .catch((error: any) => { + logger.error( + `Error checking bandwidth limits for org ${orgId}:`, + error + ); + }); } - } 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) { - const [updatedSite] = await trx - .update(sites) - .set({ - online: newOnlineStatus - }) - .where(eq(sites.siteId, site.siteId)) - .returning(); + // Process uptime usage for this org + const totalUptime = orgUptimeMap.get(orgId); + if (totalUptime) { + const uptimeUsage = await usageService.add( + orgId, + FeatureId.SITE_UPTIME, + totalUptime + ); + if (uptimeUsage) { + // Fire and forget - don't block on limit checking + usageService + .checkLimitSet( + orgId, + true, + FeatureId.SITE_UPTIME, + uptimeUsage + ) + .catch((error: any) => { + logger.error( + `Error checking uptime limits for org ${orgId}:`, + error + ); + }); + } + } + } catch (error) { + logger.error(`Error processing usage for org ${orgId}:`, error); + // Continue with other orgs + } + } + } + + // Handle sites that reported zero bandwidth but need online status updated + const zeroBandwidthPeers = sortedBandwidthData.filter( + (peer) => peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) + ); + + if (zeroBandwidthPeers.length > 0) { + // Fetch all zero bandwidth sites in one query + const zeroBandwidthSites = await db + .select() + .from(sites) + .where( + inArray( + sites.pubKey, + zeroBandwidthPeers.map((p) => p.publicKey) + ) + ); + + // Sort by siteId to ensure consistent lock ordering + const sortedZeroBandwidthSites = zeroBandwidthSites.sort( + (a, b) => a.siteId - b.siteId + ); + + for (const site of sortedZeroBandwidthSites) { + 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; + } + + // Only update online status if it changed + if (site.online !== newOnlineStatus) { + try { + const updatedSite = await withDeadlockRetry(async () => { + const [result] = await db + .update(sites) + .set({ + online: newOnlineStatus + }) + .where(eq(sites.siteId, site.siteId)) + .returning(); + return result; + }, `update offline status for site ${site.siteId}`); if (updatedSite && exitNodeId) { - if ( - await checkExitNodeOrg( - exitNodeId, - updatedSite.orgId, - trx - ) - ) { - // not allowed + const notAllowed = await checkExitNodeOrg( + exitNodeId, + updatedSite.orgId + ); + if (notAllowed) { 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"); } } @@ -262,8 +319,14 @@ export async function updateSiteBandwidth( if (!newOnlineStatus && site.pubKey) { offlineSites.add(site.pubKey); } + } catch (error) { + logger.error( + `Failed to update offline status for site ${site.siteId}:`, + error + ); + // Continue with other sites } } } - }); + } } diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index e1fa7c4c..3f24430b 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -21,6 +21,7 @@ import { validateOlmSessionToken } from "@server/auth/sessions/olm"; import { checkExitNodeOrg } from "#dynamic/lib/exitNodes"; import { updatePeer as updateOlmPeer } from "../olm/peers"; import { updatePeer as updateNewtPeer } from "../newt/peers"; +import { formatEndpoint } from "@server/lib/ip"; // Define Zod schema for request validation const updateHolePunchSchema = z.object({ @@ -207,9 +208,12 @@ export async function updateAndGenerateEndpointDestinations( // `Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}` // ); + // Format the endpoint properly for both IPv4 and IPv6 + const formattedEndpoint = formatEndpoint(ip, port); + // if the public key or endpoint has changed, update it otherwise continue if ( - site.endpoint === `${ip}:${port}` && + site.endpoint === formattedEndpoint && site.publicKey === publicKey ) { continue; @@ -218,7 +222,7 @@ export async function updateAndGenerateEndpointDestinations( const [updatedClientSitesAssociationsCache] = await db .update(clientSitesAssociationsCache) .set({ - endpoint: `${ip}:${port}`, + endpoint: formattedEndpoint, publicKey: publicKey }) .where( @@ -310,11 +314,14 @@ export async function updateAndGenerateEndpointDestinations( currentSiteId = newt.siteId; + // Format the endpoint properly for both IPv4 and IPv6 + const formattedSiteEndpoint = formatEndpoint(ip, port); + // Update the current site with the new endpoint const [updatedSite] = await db .update(sites) .set({ - endpoint: `${ip}:${port}`, + endpoint: formattedSiteEndpoint, lastHolePunch: timestamp }) .where(eq(sites.siteId, newt.siteId)) diff --git a/server/routers/hybrid.ts b/server/routers/hybrid.ts index 235961f1..398abdb8 100644 --- a/server/routers/hybrid.ts +++ b/server/routers/hybrid.ts @@ -1,4 +1,4 @@ import { Router } from "express"; // Root routes -export const hybridRouter = Router(); \ No newline at end of file +export const hybridRouter = Router(); diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts index b8c947b0..b9a0098b 100644 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -12,14 +12,14 @@ import { eq, and } from "drizzle-orm"; import { idp, idpOrg } from "@server/db"; const paramsSchema = z.strictObject({ - idpId: z.coerce.number(), - orgId: z.string() - }); + idpId: z.coerce.number(), + orgId: z.string() +}); const bodySchema = z.strictObject({ - roleMapping: z.string().optional(), - orgMapping: z.string().optional() - }); + roleMapping: z.string().optional(), + orgMapping: z.string().optional() +}); export type CreateIdpOrgPolicyResponse = {}; diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 2548cb04..c7eeaf30 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -15,17 +15,17 @@ import config from "@server/lib/config"; const paramsSchema = z.strictObject({}); const bodySchema = z.strictObject({ - name: z.string().nonempty(), - clientId: z.string().nonempty(), - clientSecret: z.string().nonempty(), - authUrl: z.url(), - tokenUrl: z.url(), - identifierPath: z.string().nonempty(), - emailPath: z.string().optional(), - namePath: z.string().optional(), - scopes: z.string().nonempty(), - autoProvision: z.boolean().optional() - }); + name: z.string().nonempty(), + clientId: z.string().nonempty(), + clientSecret: z.string().nonempty(), + authUrl: z.url(), + tokenUrl: z.url(), + identifierPath: z.string().nonempty(), + emailPath: z.string().optional(), + namePath: z.string().optional(), + scopes: z.string().nonempty(), + autoProvision: z.boolean().optional() +}); export type CreateIdpResponse = { idpId: number; diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index 56c0ca98..f2b55099 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -53,12 +53,7 @@ export async function deleteIdp( .where(eq(idp.idpId, idpId)); if (!existingIdp) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "IdP not found" - ) - ); + return next(createHttpError(HttpCode.NOT_FOUND, "IdP not found")); } // Delete the IDP and its related records in a transaction @@ -69,14 +64,10 @@ export async function deleteIdp( .where(eq(idpOidcConfig.idpId, idpId)); // Delete IDP-org mappings - await trx - .delete(idpOrg) - .where(eq(idpOrg.idpId, idpId)); + await trx.delete(idpOrg).where(eq(idpOrg.idpId, idpId)); // Delete the IDP itself - await trx - .delete(idp) - .where(eq(idp.idpId, idpId)); + await trx.delete(idp).where(eq(idp.idpId, idpId)); }); return response(res, { diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts index c5f18282..b52a37df 100644 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -11,9 +11,9 @@ import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.strictObject({ - idpId: z.coerce.number(), - orgId: z.string() - }); + idpId: z.coerce.number(), + orgId: z.string() +}); registry.registerPath({ method: "delete", diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 2db8783f..50b63ee5 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -24,8 +24,8 @@ const paramsSchema = z .strict(); const bodySchema = z.strictObject({ - redirectUrl: z.string() - }); + redirectUrl: z.string() +}); const querySchema = z.object({ orgId: z.string().optional() // check what actuall calls it diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts index e8651c84..07253751 100644 --- a/server/routers/idp/getIdp.ts +++ b/server/routers/idp/getIdp.ts @@ -71,14 +71,8 @@ export async function getIdp( const clientSecret = idpRes.idpOidcConfig!.clientSecret; const clientId = idpRes.idpOidcConfig!.clientId; - idpRes.idpOidcConfig!.clientSecret = decrypt( - clientSecret, - key - ); - idpRes.idpOidcConfig!.clientId = decrypt( - clientId, - key - ); + idpRes.idpOidcConfig!.clientSecret = decrypt(clientSecret, key); + idpRes.idpOidcConfig!.clientId = decrypt(clientId, key); } return response(res, { diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index 81cec8d1..f0dcf02e 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -8,4 +8,4 @@ export * from "./getIdp"; export * from "./createIdpOrgPolicy"; export * from "./deleteIdpOrgPolicy"; export * from "./listIdpOrgPolicies"; -export * from "./updateIdpOrgPolicy"; \ No newline at end of file +export * from "./updateIdpOrgPolicy"; diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts index 087b52f8..9f7cdb42 100644 --- a/server/routers/idp/listIdpOrgPolicies.ts +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -15,19 +15,19 @@ const paramsSchema = z.object({ }); const querySchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function query(idpId: number, limit: number, offset: number) { const res = await db diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts index 8ce2ab78..20d1899e 100644 --- a/server/routers/idp/listIdps.ts +++ b/server/routers/idp/listIdps.ts @@ -11,19 +11,19 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const querySchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function query(limit: number, offset: number) { const res = await db diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts index 82d3b5f2..6432faf6 100644 --- a/server/routers/idp/updateIdpOrgPolicy.ts +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -11,14 +11,14 @@ import { eq, and } from "drizzle-orm"; import { idp, idpOrg } from "@server/db"; const paramsSchema = z.strictObject({ - idpId: z.coerce.number(), - orgId: z.string() - }); + idpId: z.coerce.number(), + orgId: z.string() +}); const bodySchema = z.strictObject({ - roleMapping: z.string().optional(), - orgMapping: z.string().optional() - }); + roleMapping: z.string().optional(), + orgMapping: z.string().optional() +}); export type UpdateIdpOrgPolicyResponse = {}; diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 1dbdd00a..a4d55187 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -19,19 +19,19 @@ const paramsSchema = z .strict(); const bodySchema = z.strictObject({ - name: z.string().optional(), - clientId: z.string().optional(), - clientSecret: z.string().optional(), - authUrl: z.string().optional(), - tokenUrl: z.string().optional(), - identifierPath: z.string().optional(), - emailPath: z.string().optional(), - namePath: z.string().optional(), - scopes: z.string().optional(), - autoProvision: z.boolean().optional(), - defaultRoleMapping: z.string().optional(), - defaultOrgMapping: z.string().optional() - }); + name: z.string().optional(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), + authUrl: z.string().optional(), + tokenUrl: z.string().optional(), + identifierPath: z.string().optional(), + emailPath: z.string().optional(), + namePath: z.string().optional(), + scopes: z.string().optional(), + autoProvision: z.boolean().optional(), + defaultRoleMapping: z.string().optional(), + defaultOrgMapping: z.string().optional() +}); export type UpdateIdpResponse = { idpId: number; diff --git a/server/routers/license/types.ts b/server/routers/license/types.ts index 945bd368..a78a287f 100644 --- a/server/routers/license/types.ts +++ b/server/routers/license/types.ts @@ -8,4 +8,4 @@ export type GetLicenseStatusResponse = LicenseStatus; export type ListLicenseKeysResponse = LicenseKeyCache[]; -export type RecheckStatusResponse = LicenseStatus; \ No newline at end of file +export type RecheckStatusResponse = LicenseStatus; diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts index 26f59cab..a68dd7d4 100644 --- a/server/routers/loginPage/types.ts +++ b/server/routers/loginPage/types.ts @@ -8,4 +8,4 @@ export type GetLoginPageResponse = LoginPage; export type UpdateLoginPageResponse = LoginPage; -export type LoadLoginPageResponse = LoginPage & { orgId: string }; \ No newline at end of file +export type LoadLoginPageResponse = LoginPage & { orgId: string }; diff --git a/server/routers/newt/createNewt.ts b/server/routers/newt/createNewt.ts index 930c04be..b5da405e 100644 --- a/server/routers/newt/createNewt.ts +++ b/server/routers/newt/createNewt.ts @@ -24,9 +24,9 @@ export type CreateNewtResponse = { }; const createNewtSchema = z.strictObject({ - newtId: z.string(), - secret: z.string() - }); + newtId: z.string(), + secret: z.string() +}); export async function createNewt( req: Request, @@ -34,7 +34,6 @@ export async function createNewt( next: NextFunction ): Promise { try { - const parsedBody = createNewtSchema.safeParse(req.body); if (!parsedBody.success) { return next( @@ -58,7 +57,7 @@ export async function createNewt( await db.insert(newts).values({ newtId: newtId, secretHash, - dateCreated: moment().toISOString(), + dateCreated: moment().toISOString() }); // give the newt their default permissions: @@ -75,12 +74,12 @@ export async function createNewt( data: { newtId, secret, - token, + token }, success: true, error: false, message: "Newt created successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (e) { if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { diff --git a/server/routers/newt/getNewtToken.ts b/server/routers/newt/getNewtToken.ts index 3bf45dcf..63797358 100644 --- a/server/routers/newt/getNewtToken.ts +++ b/server/routers/newt/getNewtToken.ts @@ -15,6 +15,7 @@ import { import { verifyPassword } from "@server/auth/password"; import logger from "@server/logger"; import config from "@server/lib/config"; +import { APP_VERSION } from "@server/lib/consts"; export const newtGetTokenBodySchema = z.object({ newtId: z.string(), @@ -94,9 +95,10 @@ export async function getNewtToken( const resToken = generateSessionToken(); await createNewtSession(resToken, existingNewt.newtId); - return response<{ token: string }>(res, { + return response<{ token: string; serverVersion: string }>(res, { data: { - token: resToken + token: resToken, + serverVersion: APP_VERSION }, success: true, error: false, diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 5f42cd82..bfe14ec5 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -11,7 +11,7 @@ import { } from "@server/db"; import { clients, clientSitesAssociationsCache, Newt, sites } from "@server/db"; import { eq } from "drizzle-orm"; -import { updatePeer } from "../olm/peers"; +import { initPeerAddHandshake, updatePeer } from "../olm/peers"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { generateSubnetProxyTargets, SubnetProxyTarget } from "@server/lib/ip"; import config from "@server/lib/config"; @@ -140,92 +140,101 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { ) .where(eq(clientSitesAssociationsCache.siteId, siteId)); - // Prepare peers data for the response - const peers = await Promise.all( - clientsRes - .filter((client) => { - if (!client.clients.pubKey) { - logger.warn( - `Client ${client.clients.clientId} has no public key, skipping` - ); - return false; - } - if (!client.clients.subnet) { - logger.warn( - `Client ${client.clients.clientId} has no subnet, skipping` - ); - return false; - } - return true; - }) - .map(async (client) => { - // Add or update this peer on the olm if it is connected - if (!site.publicKey) { - logger.warn( - `Site ${site.siteId} has no public key, skipping` - ); - return null; - } + let peers: Array<{ + publicKey: string; + allowedIps: string[]; + endpoint?: string; + }> = []; - if (!exitNode) { - logger.warn(`Exit node not found for site ${site.siteId}`); - return null; - } + if (site.publicKey && site.endpoint && exitNode) { + // Prepare peers data for the response + peers = await Promise.all( + clientsRes + .filter((client) => { + if (!client.clients.pubKey) { + logger.warn( + `Client ${client.clients.clientId} has no public key, skipping` + ); + return false; + } + if (!client.clients.subnet) { + logger.warn( + `Client ${client.clients.clientId} has no subnet, skipping` + ); + return false; + } + return true; + }) + .map(async (client) => { + // Add or update this peer on the olm if it is connected - if (!site.endpoint) { - logger.warn( - `Site ${site.siteId} has no endpoint, skipping` - ); - return null; - } - - // const allSiteResources = await db // only get the site resources that this client has access to - // .select() - // .from(siteResources) - // .innerJoin( - // clientSiteResourcesAssociationsCache, - // eq( - // siteResources.siteResourceId, - // clientSiteResourcesAssociationsCache.siteResourceId - // ) - // ) - // .where( - // and( - // eq(siteResources.siteId, site.siteId), - // eq( - // clientSiteResourcesAssociationsCache.clientId, - // client.clients.clientId - // ) - // ) - // ); - await updatePeer(client.clients.clientId, { - siteId: site.siteId, - endpoint: site.endpoint, - relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`, - publicKey: site.publicKey, - serverIP: site.address, - serverPort: site.listenPort - // remoteSubnets: generateRemoteSubnets( - // allSiteResources.map( - // ({ siteResources }) => siteResources + // const allSiteResources = await db // only get the site resources that this client has access to + // .select() + // .from(siteResources) + // .innerJoin( + // clientSiteResourcesAssociationsCache, + // eq( + // siteResources.siteResourceId, + // clientSiteResourcesAssociationsCache.siteResourceId + // ) // ) - // ), - // aliases: generateAliasConfig( - // allSiteResources.map( - // ({ siteResources }) => siteResources - // ) - // ) - }); + // .where( + // and( + // eq(siteResources.siteId, site.siteId), + // eq( + // clientSiteResourcesAssociationsCache.clientId, + // client.clients.clientId + // ) + // ) + // ); - return { - publicKey: client.clients.pubKey!, - allowedIps: [`${client.clients.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: client.clientSitesAssociationsCache.isRelayed - ? "" - : client.clientSitesAssociationsCache.endpoint! // if its relayed it should be localhost - }; - }) - ); + // update the peer info on the olm + // if the peer has not been added yet this will be a no-op + await updatePeer(client.clients.clientId, { + siteId: site.siteId, + endpoint: site.endpoint!, + relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`, + publicKey: site.publicKey!, + serverIP: site.address, + serverPort: site.listenPort + // remoteSubnets: generateRemoteSubnets( + // allSiteResources.map( + // ({ siteResources }) => siteResources + // ) + // ), + // aliases: generateAliasConfig( + // allSiteResources.map( + // ({ siteResources }) => siteResources + // ) + // ) + }); + + // also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch + // if it has already been added this will be a no-op + await initPeerAddHandshake( + // this will kick off the add peer process for the client + client.clients.clientId, + { + siteId, + exitNode: { + publicKey: exitNode.publicKey, + endpoint: exitNode.endpoint + } + } + ); + + return { + publicKey: client.clients.pubKey!, + allowedIps: [ + `${client.clients.subnet.split("/")[0]}/32` + ], // we want to only allow from that client + endpoint: client.clientSitesAssociationsCache.isRelayed + ? "" + : client.clientSitesAssociationsCache.endpoint! // if its relayed it should be localhost + }; + }) + ); + } // Filter out any null values from peers that didn't have an olm const validPeers = peers.filter((peer) => peer !== null); diff --git a/server/routers/newt/handleNewtPingRequestMessage.ts b/server/routers/newt/handleNewtPingRequestMessage.ts index fea157fd..b75ddd5e 100644 --- a/server/routers/newt/handleNewtPingRequestMessage.ts +++ b/server/routers/newt/handleNewtPingRequestMessage.ts @@ -35,7 +35,11 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => { const { noCloud } = message.data; - const exitNodesList = await listExitNodes(site.orgId, true, noCloud || false); // filter for only the online ones + const exitNodesList = await listExitNodes( + site.orgId, + true, + noCloud || false + ); // filter for only the online ones let lastExitNodeId = null; if (newt.siteId) { diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index f4d963a1..77e49a20 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -255,7 +255,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { hcTimeout: targetHealthCheck.hcTimeout, hcHeaders: targetHealthCheck.hcHeaders, hcMethod: targetHealthCheck.hcMethod, - hcTlsServerName: targetHealthCheck.hcTlsServerName, + hcTlsServerName: targetHealthCheck.hcTlsServerName }) .from(targets) .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) @@ -328,7 +328,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { hcTimeout: target.hcTimeout, // in seconds hcHeaders: hcHeadersSend, hcMethod: target.hcMethod, - hcTlsServerName: target.hcTlsServerName, + hcTlsServerName: target.hcTlsServerName }; }); @@ -366,7 +366,7 @@ async function getUniqueSubnetForSite( trx: Transaction | typeof db = db ): Promise { const lockKey = `subnet-allocation:${exitNode.exitNodeId}`; - + return await lockManager.withLock( lockKey, async () => { @@ -382,7 +382,8 @@ async function getUniqueSubnetForSite( .map((site) => site.subnet) .filter( (subnet) => - subnet && /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet) + subnet && + /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet) ) .filter((subnet) => subnet !== null); subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`)); diff --git a/server/routers/newt/handleReceiveBandwidthMessage.ts b/server/routers/newt/handleReceiveBandwidthMessage.ts index f5170feb..3d060a0c 100644 --- a/server/routers/newt/handleReceiveBandwidthMessage.ts +++ b/server/routers/newt/handleReceiveBandwidthMessage.ts @@ -10,7 +10,9 @@ interface PeerBandwidth { bytesOut: number; } -export const handleReceiveBandwidthMessage: MessageHandler = async (context) => { +export const handleReceiveBandwidthMessage: MessageHandler = async ( + context +) => { const { message, client, sendToClient } = context; if (!message.data.bandwidthData) { @@ -44,7 +46,7 @@ export const handleReceiveBandwidthMessage: MessageHandler = async (context) => .set({ megabytesOut: (client.megabytesIn || 0) + bytesIn, megabytesIn: (client.megabytesOut || 0) + bytesOut, - lastBandwidthUpdate: new Date().toISOString(), + lastBandwidthUpdate: new Date().toISOString() }) .where(eq(clients.clientId, client.clientId)); } diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts index 09a473b9..f26f69c9 100644 --- a/server/routers/newt/handleSocketMessages.ts +++ b/server/routers/newt/handleSocketMessages.ts @@ -64,9 +64,5 @@ export const handleDockerContainersMessage: MessageHandler = async ( return; } - await applyNewtDockerBlueprint( - newt.siteId, - newt.newtId, - containers - ); + await applyNewtDockerBlueprint(newt.siteId, newt.newtId, containers); }; diff --git a/server/routers/newt/index.ts b/server/routers/newt/index.ts index 9642a637..6b17f324 100644 --- a/server/routers/newt/index.ts +++ b/server/routers/newt/index.ts @@ -5,4 +5,4 @@ export * from "./handleReceiveBandwidthMessage"; export * from "./handleGetConfigMessage"; export * from "./handleSocketMessages"; export * from "./handleNewtPingRequestMessage"; -export * from "./handleApplyBlueprintMessage"; \ No newline at end of file +export * from "./handleApplyBlueprintMessage"; diff --git a/server/routers/newt/peers.ts b/server/routers/newt/peers.ts index 694f0c0f..c7546ff0 100644 --- a/server/routers/newt/peers.ts +++ b/server/routers/newt/peers.ts @@ -48,7 +48,11 @@ export async function addPeer( return site; } -export async function deletePeer(siteId: number, publicKey: string, newtId?: string) { +export async function deletePeer( + siteId: number, + publicKey: string, + newtId?: string +) { let site: Site | null = null; if (!newtId) { [site] = await db diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 32145fcb..e97aed35 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -26,22 +26,32 @@ export async function addTargets( // Create a map for quick lookup const healthCheckMap = new Map(); - healthCheckData.forEach(hc => { + healthCheckData.forEach((hc) => { healthCheckMap.set(hc.targetId, hc); }); const healthCheckTargets = targets.map((target) => { const hc = healthCheckMap.get(target.targetId); - + // If no health check data found, skip this target if (!hc) { - logger.warn(`No health check configuration found for target ${target.targetId}`); + logger.warn( + `No health check configuration found for target ${target.targetId}` + ); return null; } // Ensure all necessary fields are present - if (!hc.hcPath || !hc.hcHostname || !hc.hcPort || !hc.hcInterval || !hc.hcMethod) { - logger.debug(`Skipping target ${target.targetId} due to missing health check fields`); + if ( + !hc.hcPath || + !hc.hcHostname || + !hc.hcPort || + !hc.hcInterval || + !hc.hcMethod + ) { + logger.debug( + `Skipping target ${target.targetId} due to missing health check fields` + ); return null; // Skip targets with missing health check fields } @@ -49,9 +59,20 @@ export async function addTargets( const hcHeadersSend: { [key: string]: string } = {}; if (hcHeadersParse) { // transform - hcHeadersParse.forEach((header: { name: string; value: string }) => { - hcHeadersSend[header.name] = header.value; - }); + hcHeadersParse.forEach( + (header: { name: string; value: string }) => { + hcHeadersSend[header.name] = header.value; + } + ); + } + + // try to parse the hcStatus into a int and if not possible set to undefined + let hcStatus: number | undefined = undefined; + if (hc.hcStatus) { + const parsedStatus = parseInt(hc.hcStatus.toString()); + if (!isNaN(parsedStatus)) { + hcStatus = parsedStatus; + } } return { @@ -67,12 +88,15 @@ export async function addTargets( hcTimeout: hc.hcTimeout, // in seconds hcHeaders: hcHeadersSend, hcMethod: hc.hcMethod, - hcTlsServerName: hc.hcTlsServerName, + hcStatus: hcStatus, + hcTlsServerName: hc.hcTlsServerName }; }); // Filter out any null values from health check targets - const validHealthCheckTargets = healthCheckTargets.filter((target) => target !== null); + const validHealthCheckTargets = healthCheckTargets.filter( + (target) => target !== null + ); await sendToClient(newtId, { type: `newt/healthcheck/add`, diff --git a/server/routers/olm/createOlm.ts b/server/routers/olm/createOlm.ts index 930c04be..b5da405e 100644 --- a/server/routers/olm/createOlm.ts +++ b/server/routers/olm/createOlm.ts @@ -24,9 +24,9 @@ export type CreateNewtResponse = { }; const createNewtSchema = z.strictObject({ - newtId: z.string(), - secret: z.string() - }); + newtId: z.string(), + secret: z.string() +}); export async function createNewt( req: Request, @@ -34,7 +34,6 @@ export async function createNewt( next: NextFunction ): Promise { try { - const parsedBody = createNewtSchema.safeParse(req.body); if (!parsedBody.success) { return next( @@ -58,7 +57,7 @@ export async function createNewt( await db.insert(newts).values({ newtId: newtId, secretHash, - dateCreated: moment().toISOString(), + dateCreated: moment().toISOString() }); // give the newt their default permissions: @@ -75,12 +74,12 @@ export async function createNewt( data: { newtId, secret, - token, + token }, success: true, error: false, message: "Newt created successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (e) { if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { diff --git a/server/routers/olm/getOlmToken.ts b/server/routers/olm/getOlmToken.ts index 8e82dc94..3852b00e 100644 --- a/server/routers/olm/getOlmToken.ts +++ b/server/routers/olm/getOlmToken.ts @@ -22,6 +22,7 @@ import { import { verifyPassword } from "@server/auth/password"; import logger from "@server/logger"; import config from "@server/lib/config"; +import { APP_VERSION } from "@server/lib/consts"; export const olmGetTokenBodySchema = z.object({ olmId: z.string(), @@ -104,43 +105,13 @@ export async function getOlmToken( const resToken = generateSessionToken(); await createOlmSession(resToken, existingOlm.olmId); - let orgIdToUse = orgId; let clientIdToUse; - if (!orgIdToUse) { - if (!existingOlm.clientId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Olm is not associated with a client, orgId is required" - ) - ); - } - - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, existingOlm.clientId)) - .limit(1); - - if (!client) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Olm's associated client not found, orgId is required" - ) - ); - } - - orgIdToUse = client.orgId; - clientIdToUse = client.clientId; - } else { + if (orgId) { // we did provide the org const [client] = await db .select() .from(clients) - .where( - and(eq(clients.orgId, orgIdToUse), eq(clients.olmId, olmId)) - ) // we want to lock on to the client with this olmId otherwise it can get assigned to a random one + .where(and(eq(clients.orgId, orgId), eq(clients.olmId, olmId))) // we want to lock on to the client with this olmId otherwise it can get assigned to a random one .limit(1); if (!client) { @@ -167,6 +138,32 @@ export async function getOlmToken( .where(eq(olms.olmId, existingOlm.olmId)); } + clientIdToUse = client.clientId; + } else { + if (!existingOlm.clientId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Olm is not associated with a client, orgId is required" + ) + ); + } + + const [client] = await db + .select() + .from(clients) + .where(eq(clients.clientId, existingOlm.clientId)) + .limit(1); + + if (!client) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Olm's associated client not found, orgId is required" + ) + ); + } + clientIdToUse = client.clientId; } @@ -209,10 +206,12 @@ export async function getOlmToken( return response<{ token: string; exitNodes: { publicKey: string; endpoint: string }[]; + serverVersion: string; }>(res, { data: { token: resToken, - exitNodes: exitNodesHpData + exitNodes: exitNodesHpData, + serverVersion: APP_VERSION }, success: true, error: false, diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 632b0f3f..0fa490c8 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -7,6 +7,8 @@ import logger from "@server/logger"; import { validateSessionToken } from "@server/auth/sessions/app"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { sendTerminateClient } from "../client/terminate"; +import { encodeHexLowerCase } from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; // Track if the offline checker interval is running let offlineCheckerInterval: NodeJS.Timeout | null = null; @@ -59,9 +61,12 @@ export const startOlmOfflineChecker = (): void => { // Send a disconnect message to the client if connected try { - await sendTerminateClient(offlineClient.clientId, offlineClient.olmId); // terminate first + await sendTerminateClient( + offlineClient.clientId, + offlineClient.olmId + ); // terminate first // wait a moment to ensure the message is sent - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); await disconnectClient(offlineClient.olmId); } catch (error) { logger.error( @@ -75,7 +80,7 @@ export const startOlmOfflineChecker = (): void => { } }, OFFLINE_CHECK_INTERVAL); - logger.info("Started offline checker interval"); + logger.debug("Started offline checker interval"); }; /** @@ -133,10 +138,14 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { return; } + const sessionId = encodeHexLowerCase( + sha256(new TextEncoder().encode(userToken)) + ); + const policyCheck = await checkOrgAccessPolicy({ orgId: client.orgId, userId: olm.userId, - session: userToken // this is the user token passed in the message + sessionId // this is the user token passed in the message }); if (!policyCheck.allowed) { diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 2b30cbcf..0f71ee8b 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,17 +1,8 @@ import { - Client, clientSiteResourcesAssociationsCache, db, - ExitNode, - Org, orgs, - roleClients, - roles, - siteResources, - Transaction, - userClients, - userOrgs, - users + siteResources } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; import { @@ -25,16 +16,13 @@ import { import { and, eq, inArray, isNull } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; -import { listExitNodes } from "#dynamic/lib/exitNodes"; -import { - generateAliasConfig, - getNextAvailableClientSubnet -} from "@server/lib/ip"; +import { generateAliasConfig } from "@server/lib/ip"; import { generateRemoteSubnets } from "@server/lib/ip"; -import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { validateSessionToken } from "@server/auth/sessions/app"; import config from "@server/lib/config"; +import { encodeHexLowerCase } from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.info("Handling register olm message!"); @@ -48,7 +36,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - const { publicKey, relay, olmVersion, olmAgent, orgId, userToken } = message.data; + const { publicKey, relay, olmVersion, olmAgent, orgId, userToken } = + message.data; if (!olm.clientId) { logger.warn("Olm client ID not found"); @@ -94,10 +83,14 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } + const sessionId = encodeHexLowerCase( + sha256(new TextEncoder().encode(userToken)) + ); + const policyCheck = await checkOrgAccessPolicy({ orgId: orgId, userId: olm.userId, - session: userToken // this is the user token passed in the message + sessionId // this is the user token passed in the message }); if (!policyCheck.allowed) { @@ -117,7 +110,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - if ((olmVersion && olm.version !== olmVersion) || (olmAgent && olm.agent !== olmAgent)) { + if ( + (olmVersion && olm.version !== olmVersion) || + (olmAgent && olm.agent !== olmAgent) + ) { await db .update(olms) .set({ @@ -175,7 +171,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } // Process each site - for (const { sites: site, clientSitesAssociationsCache: association } of sitesData) { + for (const { + sites: site, + clientSitesAssociationsCache: association + } of sitesData) { if (!site.exitNodeId) { logger.warn( `Site ${site.siteId} does not have exit node, skipping` @@ -275,6 +274,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // Add site configuration to the array siteConfigurations.push({ siteId: site.siteId, + name: site.name, // relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing endpoint: site.endpoint, publicKey: site.publicKey, diff --git a/server/routers/olm/handleOlmServerPeerAddMessage.ts b/server/routers/olm/handleOlmServerPeerAddMessage.ts index 83f7ad8c..53f3474c 100644 --- a/server/routers/olm/handleOlmServerPeerAddMessage.ts +++ b/server/routers/olm/handleOlmServerPeerAddMessage.ts @@ -113,14 +113,14 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async ( .select() .from(clientSitesAssociationsCache) .where( - and( + and( eq(clientSitesAssociationsCache.clientId, client.clientId), isNotNull(clientSitesAssociationsCache.endpoint), eq(clientSitesAssociationsCache.publicKey, client.pubKey) // limit it to the current session its connected with otherwise the endpoint could be stale ) ); - // pick an endpoint + // pick an endpoint for (const assoc of currentSessionSiteAssociationCaches) { if (assoc.endpoint) { endpoint = assoc.endpoint; @@ -169,6 +169,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async ( type: "olm/wg/peer/add", data: { siteId: site.siteId, + name: site.name, endpoint: site.endpoint, publicKey: site.publicKey, serverIP: site.address, diff --git a/server/routers/olm/index.ts b/server/routers/olm/index.ts index e671dd42..594ef9cb 100644 --- a/server/routers/olm/index.ts +++ b/server/routers/olm/index.ts @@ -8,4 +8,4 @@ export * from "./listUserOlms"; export * from "./deleteUserOlm"; export * from "./getUserOlm"; export * from "./handleOlmServerPeerAddMessage"; -export * from "./handleOlmUnRelayMessage"; \ No newline at end of file +export * from "./handleOlmUnRelayMessage"; diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 2ef7a772..4aa8edd7 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -1,14 +1,14 @@ -import { db } from "@server/db"; -import { clients, olms, newts, sites } from "@server/db"; -import { eq } from "drizzle-orm"; import { sendToClient } from "#dynamic/routers/ws"; +import { db, olms } from "@server/db"; import logger from "@server/logger"; +import { eq } from "drizzle-orm"; import { Alias } from "yaml"; export async function addPeer( clientId: number, peer: { siteId: number; + name: string; publicKey: string; endpoint: string; relayEndpoint: string; @@ -35,6 +35,7 @@ export async function addPeer( type: "olm/wg/peer/add", data: { siteId: peer.siteId, + name: peer.name, publicKey: peer.publicKey, endpoint: peer.endpoint, relayEndpoint: peer.relayEndpoint, @@ -102,7 +103,7 @@ export async function updatePeer( .where(eq(olms.clientId, clientId)) .limit(1); if (!olm) { - return + return; } olmId = olm.olmId; } @@ -162,5 +163,7 @@ export async function initPeerAddHandshake( logger.warn(`Error sending message:`, error); }); - logger.info(`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`); + logger.info( + `Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}` + ); } diff --git a/server/routers/org/checkId.ts b/server/routers/org/checkId.ts index 2a898c30..f11809d2 100644 --- a/server/routers/org/checkId.ts +++ b/server/routers/org/checkId.ts @@ -10,8 +10,8 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const getOrgSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export async function checkId( req: Request, diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index 38a1c6ba..a30dcc1c 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -11,8 +11,8 @@ import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const getOrgSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export type GetOrgResponse = { org: Org; diff --git a/server/routers/org/getOrgOverview.ts b/server/routers/org/getOrgOverview.ts index dc704d6a..d368d1b3 100644 --- a/server/routers/org/getOrgOverview.ts +++ b/server/routers/org/getOrgOverview.ts @@ -19,8 +19,8 @@ import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; const getOrgParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export type GetOrgOverviewResponse = { orgName: string; diff --git a/server/routers/org/listUserOrgs.ts b/server/routers/org/listUserOrgs.ts index eb500250..103b1023 100644 --- a/server/routers/org/listUserOrgs.ts +++ b/server/routers/org/listUserOrgs.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, roles } from "@server/db"; import { Org, orgs, userOrgs } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -40,7 +40,7 @@ const listOrgsSchema = z.object({ // responses: {} // }); -type ResponseOrg = Org & { isOwner?: boolean }; +type ResponseOrg = Org & { isOwner?: boolean; isAdmin?: boolean }; export type ListUserOrgsResponse = { orgs: ResponseOrg[]; @@ -112,6 +112,7 @@ export async function listUserOrgs( userOrgs, and(eq(userOrgs.orgId, orgs.orgId), eq(userOrgs.userId, userId)) ) + .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) .limit(limit) .offset(offset); @@ -128,6 +129,9 @@ export async function listUserOrgs( if (val.userOrgs && val.userOrgs.isOwner) { res.isOwner = val.userOrgs.isOwner; } + if (val.roles && val.roles.isAdmin) { + res.isAdmin = val.roles.isAdmin; + } return res; }); diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 6e7a9b35..aa9e2151 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -16,10 +16,11 @@ import { TierId } from "@server/lib/billing/tiers"; import { cache } from "@server/lib/cache"; const updateOrgParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); -const updateOrgBodySchema = z.strictObject({ +const updateOrgBodySchema = z + .strictObject({ name: z.string().min(1).max(255).optional(), requireTwoFactor: z.boolean().optional(), maxSessionLengthHours: z.number().nullable().optional(), diff --git a/server/routers/orgIdp/types.ts b/server/routers/orgIdp/types.ts index a8e205cc..f6f581ee 100644 --- a/server/routers/orgIdp/types.ts +++ b/server/routers/orgIdp/types.ts @@ -6,10 +6,10 @@ export type CreateOrgIdpResponse = { }; export type GetOrgIdpResponse = { - idp: Idp, - idpOidcConfig: IdpOidcConfig | null, - redirectUrl: string -} + idp: Idp; + idpOidcConfig: IdpOidcConfig | null; + redirectUrl: string; +}; export type ListOrgIdpsResponse = { idps: { @@ -18,7 +18,7 @@ export type ListOrgIdpsResponse = { name: string; type: string; variant: string; - }[], + }[]; pagination: { total: number; limit: number; diff --git a/server/routers/remoteExitNode/types.ts b/server/routers/remoteExitNode/types.ts index ae0c2130..25a7d6c5 100644 --- a/server/routers/remoteExitNode/types.ts +++ b/server/routers/remoteExitNode/types.ts @@ -6,11 +6,6 @@ export type CreateRemoteExitNodeResponse = { secret: string; }; -export type UpdateRemoteExitNodeResponse = { - remoteExitNodeId: string; - secret: string; -} - export type PickRemoteExitNodeDefaultsResponse = { remoteExitNodeId: string; secret: string; @@ -36,4 +31,14 @@ export type ListRemoteExitNodesResponse = { pagination: { total: number; limit: number; offset: number }; }; -export type GetRemoteExitNodeResponse = { remoteExitNodeId: string; dateCreated: string; version: string | null; exitNodeId: number | null; name: string; address: string; endpoint: string; online: boolean; type: string | null; } \ No newline at end of file +export type GetRemoteExitNodeResponse = { + remoteExitNodeId: string; + dateCreated: string; + version: string | null; + exitNodeId: number | null; + name: string; + address: string; + endpoint: string; + online: boolean; + type: string | null; +}; diff --git a/server/routers/resource/addEmailToResourceWhitelist.ts b/server/routers/resource/addEmailToResourceWhitelist.ts index f9cee838..53828b44 100644 --- a/server/routers/resource/addEmailToResourceWhitelist.ts +++ b/server/routers/resource/addEmailToResourceWhitelist.ts @@ -11,21 +11,19 @@ import { and, eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const addEmailToResourceWhitelistBodySchema = z.strictObject({ - email: z.email() - .or( - z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { - error: "Invalid email address. Wildcard (*) must be the entire local part." - }) - ) - .transform((v) => v.toLowerCase()) - }); + email: z + .email() + .or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + error: "Invalid email address. Wildcard (*) must be the entire local part." + }) + ) + .transform((v) => v.toLowerCase()) +}); const addEmailToResourceWhitelistParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "post", diff --git a/server/routers/resource/addRoleToResource.ts b/server/routers/resource/addRoleToResource.ts index c29f2757..ba344c6c 100644 --- a/server/routers/resource/addRoleToResource.ts +++ b/server/routers/resource/addRoleToResource.ts @@ -93,10 +93,7 @@ export async function addRoleToResource( .select() .from(roles) .where( - and( - eq(roles.roleId, roleId), - eq(roles.orgId, resource.orgId) - ) + and(eq(roles.roleId, roleId), eq(roles.orgId, resource.orgId)) ) .limit(1); @@ -158,4 +155,3 @@ export async function addRoleToResource( ); } } - diff --git a/server/routers/resource/addUserToResource.ts b/server/routers/resource/addUserToResource.ts index 6dbfe086..ee6081ff 100644 --- a/server/routers/resource/addUserToResource.ts +++ b/server/routers/resource/addUserToResource.ts @@ -127,4 +127,3 @@ export async function addUserToResource( ); } } - diff --git a/server/routers/resource/authWithAccessToken.ts b/server/routers/resource/authWithAccessToken.ts index 81ca7fbc..53f72cb2 100644 --- a/server/routers/resource/authWithAccessToken.ts +++ b/server/routers/resource/authWithAccessToken.ts @@ -16,17 +16,17 @@ import stoi from "@server/lib/stoi"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; const authWithAccessTokenBodySchema = z.strictObject({ - accessToken: z.string(), - accessTokenId: z.string().optional() - }); + accessToken: z.string(), + accessTokenId: z.string().optional() +}); const authWithAccessTokenParamsSchema = z.strictObject({ - resourceId: z - .string() - .optional() - .transform(stoi) - .pipe(z.int().positive().optional()) - }); + resourceId: z + .string() + .optional() + .transform(stoi) + .pipe(z.int().positive().optional()) +}); export type AuthWithAccessTokenResponse = { session?: string; diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index 4c1f2058..ecf61896 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -16,15 +16,12 @@ import config from "@server/lib/config"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; export const authWithPasswordBodySchema = z.strictObject({ - password: z.string() - }); + password: z.string() +}); export const authWithPasswordParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); export type AuthWithPasswordResponse = { session?: string; diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 59f80ee0..78e132d2 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -15,15 +15,12 @@ import config from "@server/lib/config"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; export const authWithPincodeBodySchema = z.strictObject({ - pincode: z.string() - }); + pincode: z.string() +}); export const authWithPincodeParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); export type AuthWithPincodeResponse = { session?: string; diff --git a/server/routers/resource/authWithWhitelist.ts b/server/routers/resource/authWithWhitelist.ts index 11f84043..6a2b7ee7 100644 --- a/server/routers/resource/authWithWhitelist.ts +++ b/server/routers/resource/authWithWhitelist.ts @@ -15,16 +15,13 @@ import config from "@server/lib/config"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; const authWithWhitelistBodySchema = z.strictObject({ - email: z.email().toLowerCase(), - otp: z.string().optional() - }); + email: z.email().toLowerCase(), + otp: z.string().optional() +}); const authWithWhitelistParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); export type AuthWithWhitelistResponse = { otpSent?: boolean; diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index b9ab3ce5..ba1fdba2 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -26,16 +26,17 @@ import { getUniqueResourceName } from "@server/db/names"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; const createResourceParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); -const createHttpResourceSchema = z.strictObject({ +const createHttpResourceSchema = z + .strictObject({ name: z.string().min(1).max(255), subdomain: z.string().nullable().optional(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), domainId: z.string(), - stickySession: z.boolean().optional(), + stickySession: z.boolean().optional() }) .refine( (data) => { @@ -49,7 +50,8 @@ const createHttpResourceSchema = z.strictObject({ } ); -const createRawResourceSchema = z.strictObject({ +const createRawResourceSchema = z + .strictObject({ name: z.string().min(1).max(255), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), @@ -188,7 +190,7 @@ async function createHttpResource( const { name, domainId } = parsedBody.data; const subdomain = parsedBody.data.subdomain; - const stickySession=parsedBody.data.stickySession; + const stickySession = parsedBody.data.stickySession; // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index c3e086b0..3f86665b 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -16,19 +16,16 @@ import { import { OpenAPITags, registry } from "@server/openApi"; const createResourceRuleSchema = z.strictObject({ - action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]), - value: z.string().min(1), - priority: z.int(), - enabled: z.boolean().optional() - }); + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]), + value: z.string().min(1), + priority: z.int(), + enabled: z.boolean().optional() +}); const createResourceRuleParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "put", diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index a81208a5..d8891d75 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -15,11 +15,8 @@ import { OpenAPITags, registry } from "@server/openApi"; // Define Zod schema for request parameters validation const deleteResourceSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "delete", diff --git a/server/routers/resource/deleteResourceRule.ts b/server/routers/resource/deleteResourceRule.ts index 58cb7b48..638f2e1d 100644 --- a/server/routers/resource/deleteResourceRule.ts +++ b/server/routers/resource/deleteResourceRule.ts @@ -11,12 +11,9 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const deleteResourceRuleSchema = z.strictObject({ - ruleId: z.string().transform(Number).pipe(z.int().positive()), - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + ruleId: z.string().transform(Number).pipe(z.int().positive()), + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "delete", diff --git a/server/routers/resource/getExchangeToken.ts b/server/routers/resource/getExchangeToken.ts index 8a0276a0..b0af4b7f 100644 --- a/server/routers/resource/getExchangeToken.ts +++ b/server/routers/resource/getExchangeToken.ts @@ -17,11 +17,8 @@ import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { logAccessAudit } from "#dynamic/lib/logAccessAudit"; const getExchangeTokenParams = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); export type GetExchangeTokenResponse = { requestToken: string; diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index f2ce559e..7f3e8a0e 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -12,15 +12,15 @@ import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; const getResourceSchema = z.strictObject({ - resourceId: z - .string() - .optional() - .transform(stoi) - .pipe(z.int().positive().optional()) - .optional(), - niceId: z.string().optional(), - orgId: z.string().optional() - }); + resourceId: z + .string() + .optional() + .transform(stoi) + .pipe(z.int().positive().optional()) + .optional(), + niceId: z.string().optional(), + orgId: z.string().optional() +}); async function query(resourceId?: number, niceId?: string, orgId?: string) { if (resourceId) { @@ -34,13 +34,18 @@ async function query(resourceId?: number, niceId?: string, orgId?: string) { const [res] = await db .select() .from(resources) - .where(and(eq(resources.niceId, niceId), eq(resources.orgId, orgId))) + .where( + and(eq(resources.niceId, niceId), eq(resources.orgId, orgId)) + ) .limit(1); return res; } } -export type GetResourceResponse = Omit>>, 'headers'> & { +export type GetResourceResponse = Omit< + NonNullable>>, + "headers" +> & { headers: { name: string; value: string }[] | null; }; @@ -101,7 +106,9 @@ export async function getResource( return response(res, { data: { ...resource, - headers: resource.headers ? JSON.parse(resource.headers) : resource.headers + headers: resource.headers + ? JSON.parse(resource.headers) + : resource.headers }, success: true, error: false, diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 60f8e586..fe0a38c8 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -16,8 +16,8 @@ import logger from "@server/logger"; import { build } from "@server/build"; const getResourceAuthInfoSchema = z.strictObject({ - resourceGuid: z.string() - }); + resourceGuid: z.string() +}); export type GetResourceAuthInfoResponse = { resourceId: number; diff --git a/server/routers/resource/getResourceWhitelist.ts b/server/routers/resource/getResourceWhitelist.ts index 3171352a..52cff0c7 100644 --- a/server/routers/resource/getResourceWhitelist.ts +++ b/server/routers/resource/getResourceWhitelist.ts @@ -11,11 +11,8 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const getResourceWhitelistSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); async function queryWhitelist(resourceId: number) { return await db diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts index 3dbb8c0d..68dc58a2 100644 --- a/server/routers/resource/listResourceRoles.ts +++ b/server/routers/resource/listResourceRoles.ts @@ -11,11 +11,8 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const listResourceRolesSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); async function query(resourceId: number) { return await db diff --git a/server/routers/resource/listResourceRules.ts b/server/routers/resource/listResourceRules.ts index bc2516a0..dae7922d 100644 --- a/server/routers/resource/listResourceRules.ts +++ b/server/routers/resource/listResourceRules.ts @@ -11,11 +11,8 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; const listResourceRulesParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); const listResourceRulesSchema = z.object({ limit: z diff --git a/server/routers/resource/listResourceUsers.ts b/server/routers/resource/listResourceUsers.ts index b07bcf0a..e7f73287 100644 --- a/server/routers/resource/listResourceUsers.ts +++ b/server/routers/resource/listResourceUsers.ts @@ -11,11 +11,8 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const listResourceUsersSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); async function queryUsers(resourceId: number) { return await db diff --git a/server/routers/resource/removeEmailFromResourceWhitelist.ts b/server/routers/resource/removeEmailFromResourceWhitelist.ts index c2cac2de..d60133b8 100644 --- a/server/routers/resource/removeEmailFromResourceWhitelist.ts +++ b/server/routers/resource/removeEmailFromResourceWhitelist.ts @@ -11,21 +11,19 @@ import { and, eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const removeEmailFromResourceWhitelistBodySchema = z.strictObject({ - email: z.email() - .or( - z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { - error: "Invalid email address. Wildcard (*) must be the entire local part." - }) - ) - .transform((v) => v.toLowerCase()) - }); + email: z + .email() + .or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + error: "Invalid email address. Wildcard (*) must be the entire local part." + }) + ) + .transform((v) => v.toLowerCase()) +}); const removeEmailFromResourceWhitelistParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "post", diff --git a/server/routers/resource/removeRoleFromResource.ts b/server/routers/resource/removeRoleFromResource.ts index cb44ac4a..eab7660c 100644 --- a/server/routers/resource/removeRoleFromResource.ts +++ b/server/routers/resource/removeRoleFromResource.ts @@ -49,9 +49,7 @@ export async function removeRoleFromResource( next: NextFunction ): Promise { try { - const parsedBody = removeRoleFromResourceBodySchema.safeParse( - req.body - ); + const parsedBody = removeRoleFromResourceBodySchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( @@ -95,10 +93,7 @@ export async function removeRoleFromResource( .select() .from(roles) .where( - and( - eq(roles.roleId, roleId), - eq(roles.orgId, resource.orgId) - ) + and(eq(roles.roleId, roleId), eq(roles.orgId, resource.orgId)) ) .limit(1); @@ -163,4 +158,3 @@ export async function removeRoleFromResource( ); } } - diff --git a/server/routers/resource/removeUserFromResource.ts b/server/routers/resource/removeUserFromResource.ts index 8dce7e48..9da96d3c 100644 --- a/server/routers/resource/removeUserFromResource.ts +++ b/server/routers/resource/removeUserFromResource.ts @@ -49,9 +49,7 @@ export async function removeUserFromResource( next: NextFunction ): Promise { try { - const parsedBody = removeUserFromResourceBodySchema.safeParse( - req.body - ); + const parsedBody = removeUserFromResourceBodySchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( @@ -133,4 +131,3 @@ export async function removeUserFromResource( ); } } - diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts index 87ffbacd..b89179ae 100644 --- a/server/routers/resource/setResourceHeaderAuth.ts +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -15,9 +15,9 @@ const setResourceAuthMethodsParamsSchema = z.object({ }); const setResourceAuthMethodsBodySchema = z.strictObject({ - user: z.string().min(4).max(100).nullable(), - password: z.string().min(4).max(100).nullable() - }); + user: z.string().min(4).max(100).nullable(), + password: z.string().min(4).max(100).nullable() +}); registry.registerPath({ method: "post", @@ -75,7 +75,9 @@ export async function setResourceHeaderAuth( .where(eq(resourceHeaderAuth.resourceId, resourceId)); if (user && password) { - const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64")); + const headerAuthHash = await hashPassword( + Buffer.from(`${user}:${password}`).toString("base64") + ); await trx .insert(resourceHeaderAuth) diff --git a/server/routers/resource/setResourcePassword.ts b/server/routers/resource/setResourcePassword.ts index 3f9ce9f1..9bd845a4 100644 --- a/server/routers/resource/setResourcePassword.ts +++ b/server/routers/resource/setResourcePassword.ts @@ -17,8 +17,8 @@ const setResourceAuthMethodsParamsSchema = z.object({ }); const setResourceAuthMethodsBodySchema = z.strictObject({ - password: z.string().min(4).max(100).nullable() - }); + password: z.string().min(4).max(100).nullable() +}); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourcePincode.ts b/server/routers/resource/setResourcePincode.ts index 6a88a279..0d527273 100644 --- a/server/routers/resource/setResourcePincode.ts +++ b/server/routers/resource/setResourcePincode.ts @@ -18,11 +18,11 @@ const setResourceAuthMethodsParamsSchema = z.object({ }); const setResourceAuthMethodsBodySchema = z.strictObject({ - pincode: z - .string() - .regex(/^\d{6}$/) - .or(z.null()) - }); + pincode: z + .string() + .regex(/^\d{6}$/) + .or(z.null()) +}); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourceRoles.ts b/server/routers/resource/setResourceRoles.ts index 5064c7e0..751fe4f9 100644 --- a/server/routers/resource/setResourceRoles.ts +++ b/server/routers/resource/setResourceRoles.ts @@ -11,15 +11,12 @@ import { eq, and, ne, inArray } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const setResourceRolesBodySchema = z.strictObject({ - roleIds: z.array(z.int().positive()) - }); + roleIds: z.array(z.int().positive()) +}); const setResourceRolesParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "post", @@ -113,10 +110,7 @@ export async function setResourceRoles( .select() .from(roles) .where( - and( - eq(roles.isAdmin, true), - eq(roles.orgId, resource.orgId) - ) + and(eq(roles.isAdmin, true), eq(roles.orgId, resource.orgId)) ); const adminRoleIds = adminRoles.map((role) => role.roleId); @@ -129,9 +123,9 @@ export async function setResourceRoles( ) ); } else { - await trx.delete(roleResources).where( - eq(roleResources.resourceId, resourceId) - ); + await trx + .delete(roleResources) + .where(eq(roleResources.resourceId, resourceId)); } const newRoleResources = await Promise.all( @@ -158,4 +152,3 @@ export async function setResourceRoles( ); } } - diff --git a/server/routers/resource/setResourceUsers.ts b/server/routers/resource/setResourceUsers.ts index b5eca17c..5ddceb8f 100644 --- a/server/routers/resource/setResourceUsers.ts +++ b/server/routers/resource/setResourceUsers.ts @@ -11,15 +11,12 @@ import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const setUserResourcesBodySchema = z.strictObject({ - userIds: z.array(z.string()) - }); + userIds: z.array(z.string()) +}); const setUserResourcesParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "post", diff --git a/server/routers/resource/setResourceWhitelist.ts b/server/routers/resource/setResourceWhitelist.ts index 417ef6d9..18f612f2 100644 --- a/server/routers/resource/setResourceWhitelist.ts +++ b/server/routers/resource/setResourceWhitelist.ts @@ -11,25 +11,21 @@ import { and, eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const setResourceWhitelistBodySchema = z.strictObject({ - emails: z - .array( - z.email() - .or( - z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { - error: "Invalid email address. Wildcard (*) must be the entire local part." - }) - ) + emails: z + .array( + z.email().or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + error: "Invalid email address. Wildcard (*) must be the entire local part." + }) ) - .max(50) - .transform((v) => v.map((e) => e.toLowerCase())) - }); + ) + .max(50) + .transform((v) => v.map((e) => e.toLowerCase())) +}); const setResourceWhitelistParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "post", diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index f3792e28..1dff9757 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -26,13 +26,11 @@ import { validateHeaders } from "@server/lib/validators"; import { build } from "@server/build"; const updateResourceParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); -const updateHttpResourceBodySchema = z.strictObject({ +const updateHttpResourceBodySchema = z + .strictObject({ name: z.string().min(1).max(255).optional(), niceId: z.string().min(1).max(255).optional(), subdomain: subdomainSchema.nullable().optional(), @@ -91,7 +89,8 @@ const updateHttpResourceBodySchema = z.strictObject({ export type UpdateResourceResponse = Resource; -const updateRawResourceBodySchema = z.strictObject({ +const updateRawResourceBodySchema = z + .strictObject({ name: z.string().min(1).max(255).optional(), niceId: z.string().min(1).max(255).optional(), proxyPort: z.int().min(1).max(65535).optional(), @@ -239,11 +238,11 @@ async function updateHttpResource( .select() .from(resources) .where( - and( - eq(resources.niceId, updateData.niceId), - eq(resources.orgId, resource.orgId) - ) - ); + and( + eq(resources.niceId, updateData.niceId), + eq(resources.orgId, resource.orgId) + ) + ); if ( existingResource && @@ -391,11 +390,11 @@ async function updateRawResource( .select() .from(resources) .where( - and( - eq(resources.niceId, updateData.niceId), - eq(resources.orgId, resource.orgId) - ) - ); + and( + eq(resources.niceId, updateData.niceId), + eq(resources.orgId, resource.orgId) + ) + ); if ( existingResource && diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index b92c3d07..cae3f16e 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -17,15 +17,13 @@ import { OpenAPITags, registry } from "@server/openApi"; // Define Zod schema for request parameters validation const updateResourceRuleParamsSchema = z.strictObject({ - ruleId: z.string().transform(Number).pipe(z.int().positive()), - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + ruleId: z.string().transform(Number).pipe(z.int().positive()), + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); // Define Zod schema for request body validation -const updateResourceRuleSchema = z.strictObject({ +const updateResourceRuleSchema = z + .strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(), match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]).optional(), value: z.string().min(1).optional(), diff --git a/server/routers/role/addRoleAction.ts b/server/routers/role/addRoleAction.ts index 74540b78..5c258de7 100644 --- a/server/routers/role/addRoleAction.ts +++ b/server/routers/role/addRoleAction.ts @@ -10,12 +10,12 @@ import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; const addRoleActionParamSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); const addRoleActionSchema = z.strictObject({ - actionId: z.string() - }); + actionId: z.string() +}); export async function addRoleAction( req: Request, diff --git a/server/routers/role/addRoleSite.ts b/server/routers/role/addRoleSite.ts index d33c733d..ddd1f07e 100644 --- a/server/routers/role/addRoleSite.ts +++ b/server/routers/role/addRoleSite.ts @@ -10,12 +10,12 @@ import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; const addRoleSiteParamsSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); const addRoleSiteSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()) - }); + siteId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function addRoleSite( req: Request, diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index 26573c6c..16696af4 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -12,13 +12,13 @@ import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const createRoleParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const createRoleSchema = z.strictObject({ - name: z.string().min(1).max(255), - description: z.string().optional() - }); + name: z.string().min(1).max(255), + description: z.string().optional() +}); export const defaultRoleAllowedActions: ActionsEnum[] = [ ActionsEnum.getOrg, diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts index e4d89b2f..490fe91c 100644 --- a/server/routers/role/deleteRole.ts +++ b/server/routers/role/deleteRole.ts @@ -11,12 +11,12 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const deleteRoleSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); const deelteRoleBodySchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "delete", diff --git a/server/routers/role/getRole.ts b/server/routers/role/getRole.ts index afd6e83a..a5c45996 100644 --- a/server/routers/role/getRole.ts +++ b/server/routers/role/getRole.ts @@ -11,8 +11,8 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const getRoleSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "get", diff --git a/server/routers/role/listRoleActions.ts b/server/routers/role/listRoleActions.ts index 8392c296..31ef6604 100644 --- a/server/routers/role/listRoleActions.ts +++ b/server/routers/role/listRoleActions.ts @@ -10,8 +10,8 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const listRoleActionsSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function listRoleActions( req: Request, diff --git a/server/routers/role/listRoleResources.ts b/server/routers/role/listRoleResources.ts index 57a84c5c..7ba1fdab 100644 --- a/server/routers/role/listRoleResources.ts +++ b/server/routers/role/listRoleResources.ts @@ -10,8 +10,8 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const listRoleResourcesSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function listRoleResources( req: Request, diff --git a/server/routers/role/listRoleSites.ts b/server/routers/role/listRoleSites.ts index f35e367c..1c9dcdbe 100644 --- a/server/routers/role/listRoleSites.ts +++ b/server/routers/role/listRoleSites.ts @@ -10,8 +10,8 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const listRoleSitesSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function listRoleSites( req: Request, diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index 14a5c2d1..288a540d 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -12,8 +12,8 @@ import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; const listRolesParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listRolesSchema = z.object({ limit: z diff --git a/server/routers/role/removeRoleAction.ts b/server/routers/role/removeRoleAction.ts index 25fbaa29..3c2ee788 100644 --- a/server/routers/role/removeRoleAction.ts +++ b/server/routers/role/removeRoleAction.ts @@ -10,12 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const removeRoleActionParamsSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); const removeRoleActionSchema = z.strictObject({ - actionId: z.string() - }); + actionId: z.string() +}); export async function removeRoleAction( req: Request, diff --git a/server/routers/role/removeRoleResource.ts b/server/routers/role/removeRoleResource.ts index d2c7cae9..fac1c941 100644 --- a/server/routers/role/removeRoleResource.ts +++ b/server/routers/role/removeRoleResource.ts @@ -10,15 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const removeRoleResourceParamsSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); const removeRoleResourceSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function removeRoleResource( req: Request, diff --git a/server/routers/role/removeRoleSite.ts b/server/routers/role/removeRoleSite.ts index 8092eed1..6c64820e 100644 --- a/server/routers/role/removeRoleSite.ts +++ b/server/routers/role/removeRoleSite.ts @@ -10,12 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const removeRoleSiteParamsSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); const removeRoleSiteSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()) - }); + siteId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function removeRoleSite( req: Request, diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts index 136ca389..c9f63a7b 100644 --- a/server/routers/role/updateRole.ts +++ b/server/routers/role/updateRole.ts @@ -10,10 +10,11 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const updateRoleParamsSchema = z.strictObject({ - roleId: z.string().transform(Number).pipe(z.int().positive()) - }); + roleId: z.string().transform(Number).pipe(z.int().positive()) +}); -const updateRoleBodySchema = z.strictObject({ +const updateRoleBodySchema = z + .strictObject({ name: z.string().min(1).max(255).optional(), description: z.string().optional() }) diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 2ec8d3dc..c798ea30 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -20,25 +20,25 @@ import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; const createSiteParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const createSiteSchema = z.strictObject({ - name: z.string().min(1).max(255), - exitNodeId: z.int().positive().optional(), - // subdomain: z - // .string() - // .min(1) - // .max(255) - // .transform((val) => val.toLowerCase()) - // .optional(), - pubKey: z.string().optional(), - subnet: z.string().optional(), - newtId: z.string().optional(), - secret: z.string().optional(), - address: z.string().optional(), - type: z.enum(["newt", "wireguard", "local"]) - }); + name: z.string().min(1).max(255), + exitNodeId: z.int().positive().optional(), + // subdomain: z + // .string() + // .min(1) + // .max(255) + // .transform((val) => val.toLowerCase()) + // .optional(), + pubKey: z.string().optional(), + subnet: z.string().optional(), + newtId: z.string().optional(), + secret: z.string().optional(), + address: z.string().optional(), + type: z.enum(["newt", "wireguard", "local"]) +}); // .refine((data) => { // if (data.type === "local") { // return !config.getRawConfig().flags?.disable_local_sites; diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index a086e143..09750c31 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -13,8 +13,8 @@ import { sendToClient } from "#dynamic/routers/ws"; import { OpenAPITags, registry } from "@server/openApi"; const deleteSiteSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()) - }); + siteId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "delete", @@ -93,8 +93,11 @@ export async function deleteSite( data: {} }; // Don't await this to prevent blocking the response - sendToClient(deletedNewtId, payload).catch(error => { - logger.error("Failed to send termination message to newt:", error); + sendToClient(deletedNewtId, payload).catch((error) => { + logger.error( + "Failed to send termination message to newt:", + error + ); }); } diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index b6ce346a..c82bf199 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, newts } from "@server/db"; import { sites } from "@server/db"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; @@ -12,15 +12,15 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const getSiteSchema = z.strictObject({ - siteId: z - .string() - .optional() - .transform(stoi) - .pipe(z.int().positive().optional()) - .optional(), - niceId: z.string().optional(), - orgId: z.string().optional() - }); + siteId: z + .string() + .optional() + .transform(stoi) + .pipe(z.int().positive().optional()) + .optional(), + niceId: z.string().optional(), + orgId: z.string().optional() +}); async function query(siteId?: number, niceId?: string, orgId?: string) { if (siteId) { @@ -28,6 +28,7 @@ async function query(siteId?: number, niceId?: string, orgId?: string) { .select() .from(sites) .where(eq(sites.siteId, siteId)) + .leftJoin(newts, eq(sites.siteId, newts.siteId)) .limit(1); return res; } else if (niceId && orgId) { @@ -35,12 +36,15 @@ async function query(siteId?: number, niceId?: string, orgId?: string) { .select() .from(sites) .where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId))) + .leftJoin(newts, eq(sites.siteId, newts.siteId)) .limit(1); return res; } } -export type GetSiteResponse = NonNullable>>; +export type GetSiteResponse = NonNullable< + Awaited> +>["sites"] & { newtId: string | null }; registry.registerPath({ method: "get", @@ -94,8 +98,13 @@ export async function getSite( return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); } + const data: GetSiteResponse = { + ...site.sites, + newtId: site.newt ? site.newt.newtId : null + }; + return response(res, { - data: site, + data, success: true, error: false, message: "Site retrieved successfully", diff --git a/server/routers/site/index.ts b/server/routers/site/index.ts index b97557a8..3edf67c1 100644 --- a/server/routers/site/index.ts +++ b/server/routers/site/index.ts @@ -5,4 +5,4 @@ export * from "./updateSite"; export * from "./listSites"; export * from "./listSiteRoles"; export * from "./pickSiteDefaults"; -export * from "./socketIntegration"; \ No newline at end of file +export * from "./socketIntegration"; diff --git a/server/routers/site/listSiteRoles.ts b/server/routers/site/listSiteRoles.ts index ec66d3c5..a2cacf1d 100644 --- a/server/routers/site/listSiteRoles.ts +++ b/server/routers/site/listSiteRoles.ts @@ -10,8 +10,8 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const listSiteRolesSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()) - }); + siteId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function listSiteRoles( req: Request, diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index f0854764..37ca8fe4 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -69,8 +69,8 @@ async function getLatestNewtVersion(): Promise { } const listSitesParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listSitesSchema = z.object({ limit: z diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 029ae322..69ed7688 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -45,8 +45,8 @@ registry.registerPath({ }); const pickSiteDefaultsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); export async function pickSiteDefaults( req: Request, @@ -74,7 +74,10 @@ export async function pickSiteDefaults( if (!randomExitNode) { return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "No available exit node") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "No available exit node" + ) ); } @@ -90,7 +93,10 @@ export async function pickSiteDefaults( // 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 && /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)) + .filter( + (subnet) => + subnet && /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet) + ) .filter((subnet) => subnet !== null); // exclude the exit node address by replacing after the / with a site block size subnets.push( diff --git a/server/routers/site/socketIntegration.ts b/server/routers/site/socketIntegration.ts index 33893000..e0ad09d1 100644 --- a/server/routers/site/socketIntegration.ts +++ b/server/routers/site/socketIntegration.ts @@ -10,10 +10,7 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; import stoi from "@server/lib/stoi"; import { sendToClient } from "#dynamic/routers/ws"; -import { - fetchContainers, - dockerSocket -} from "../newt/dockerSocket"; +import { fetchContainers, dockerSocket } from "../newt/dockerSocket"; import cache from "@server/lib/cache"; export interface ContainerNetwork { @@ -47,13 +44,13 @@ export interface Container { } const siteIdParamsSchema = z.strictObject({ - siteId: z.string().transform(stoi).pipe(z.int().positive()) - }); + siteId: z.string().transform(stoi).pipe(z.int().positive()) +}); const DockerStatusSchema = z.strictObject({ - isAvailable: z.boolean(), - socketPath: z.string().optional() - }); + isAvailable: z.boolean(), + socketPath: z.string().optional() +}); function validateSiteIdParams(params: any) { const parsedParams = siteIdParamsSchema.safeParse(params); @@ -161,9 +158,7 @@ async function triggerFetch(siteId: number) { async function queryContainers(siteId: number) { const { newt } = await getSiteAndNewt(siteId); - const result = cache.get( - `${newt.newtId}:dockerContainers` - ) as Container[]; + const result = cache.get(`${newt.newtId}:dockerContainers`) as Container[]; if (!result) { throw createHttpError( HttpCode.TOO_EARLY, diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index 4c25d4c5..44764362 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -12,16 +12,15 @@ import { OpenAPITags, registry } from "@server/openApi"; import { isValidCIDR } from "@server/lib/validators"; const updateSiteParamsSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()) - }); + siteId: z.string().transform(Number).pipe(z.int().positive()) +}); -const updateSiteBodySchema = z.strictObject({ +const updateSiteBodySchema = z + .strictObject({ name: z.string().min(1).max(255).optional(), niceId: z.string().min(1).max(255).optional(), dockerSocketEnabled: z.boolean().optional(), - remoteSubnets: z - .string() - .optional() + remoteSubnets: z.string().optional() // subdomain: z // .string() // .min(1) @@ -41,8 +40,7 @@ const updateSiteBodySchema = z.strictObject({ registry.registerPath({ method: "post", path: "/site/{siteId}", - description: - "Update a site.", + description: "Update a site.", tags: [OpenAPITags.Site], request: { params: updateSiteParamsSchema, @@ -111,7 +109,9 @@ export async function updateSite( // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs if (updateData.remoteSubnets) { - const subnets = updateData.remoteSubnets.split(",").map((s) => s.trim()); + const subnets = updateData.remoteSubnets + .split(",") + .map((s) => s.trim()); for (const subnet of subnets) { if (!isValidCIDR(subnet)) { return next( diff --git a/server/routers/siteResource/addClientToSiteResource.ts b/server/routers/siteResource/addClientToSiteResource.ts index 587294e5..27d7f057 100644 --- a/server/routers/siteResource/addClientToSiteResource.ts +++ b/server/routers/siteResource/addClientToSiteResource.ts @@ -28,7 +28,8 @@ const addClientToSiteResourceParamsSchema = z registry.registerPath({ method: "post", path: "/site-resource/{siteResourceId}/clients/add", - description: "Add a single client to a site resource. Clients with a userId cannot be added.", + description: + "Add a single client to a site resource. Clients with a userId cannot be added.", tags: [OpenAPITags.Resource, OpenAPITags.Client], request: { params: addClientToSiteResourceParamsSchema, @@ -49,7 +50,9 @@ export async function addClientToSiteResource( next: NextFunction ): Promise { try { - const parsedBody = addClientToSiteResourceBodySchema.safeParse(req.body); + const parsedBody = addClientToSiteResourceBodySchema.safeParse( + req.body + ); if (!parsedBody.success) { return next( createHttpError( @@ -153,4 +156,3 @@ export async function addClientToSiteResource( ); } } - diff --git a/server/routers/siteResource/addRoleToSiteResource.ts b/server/routers/siteResource/addRoleToSiteResource.ts index 542ca535..abc2d221 100644 --- a/server/routers/siteResource/addRoleToSiteResource.ts +++ b/server/routers/siteResource/addRoleToSiteResource.ts @@ -163,4 +163,3 @@ export async function addRoleToSiteResource( ); } } - diff --git a/server/routers/siteResource/addUserToSiteResource.ts b/server/routers/siteResource/addUserToSiteResource.ts index c9d1f30a..4edf741c 100644 --- a/server/routers/siteResource/addUserToSiteResource.ts +++ b/server/routers/siteResource/addUserToSiteResource.ts @@ -132,4 +132,3 @@ export async function addUserToSiteResource( ); } } - diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index b2e4b0cb..e5719e7f 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -1,24 +1,26 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; import { clientSiteResources, db, newts, roles, roleSiteResources, + SiteResource, + siteResources, + sites, userSiteResources } from "@server/db"; -import { siteResources, sites, SiteResource } from "@server/db"; +import { getUniqueSiteResourceName } from "@server/db/names"; +import { getNextAvailableAliasAddress } from "@server/lib/ip"; +import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { eq, and, is } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -import { getUniqueSiteResourceName } from "@server/db/names"; -import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; -import { getNextAvailableAliasAddress } from "@server/lib/ip"; +import HttpCode from "@server/types/HttpCode"; +import { and, eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; const createSiteResourceParamsSchema = z.strictObject({ siteId: z.string().transform(Number).pipe(z.int().positive()), diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index a7175608..3d1e70cc 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -106,7 +106,10 @@ export async function deleteSiteResource( ); } - await rebuildClientAssociationsFromSiteResource(removedSiteResource, trx); + await rebuildClientAssociationsFromSiteResource( + removedSiteResource, + trx + ); }); logger.info( diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts index 48f10b8b..7cb9e620 100644 --- a/server/routers/siteResource/getSiteResource.ts +++ b/server/routers/siteResource/getSiteResource.ts @@ -11,44 +11,55 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; const getSiteResourceParamsSchema = z.strictObject({ - siteResourceId: z - .string() - .optional() - .transform((val) => val ? Number(val) : undefined) - .pipe(z.int().positive().optional()) - .optional(), - siteId: z.string().transform(Number).pipe(z.int().positive()), - niceId: z.string().optional(), - orgId: z.string() - }); + siteResourceId: z + .string() + .optional() + .transform((val) => (val ? Number(val) : undefined)) + .pipe(z.int().positive().optional()) + .optional(), + siteId: z.string().transform(Number).pipe(z.int().positive()), + niceId: z.string().optional(), + orgId: z.string() +}); -async function query(siteResourceId?: number, siteId?: number, niceId?: string, orgId?: string) { +async function query( + siteResourceId?: number, + siteId?: number, + niceId?: string, + orgId?: string +) { if (siteResourceId && siteId && orgId) { const [siteResource] = await db .select() .from(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) + .where( + and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) + ) .limit(1); return siteResource; } else if (niceId && siteId && orgId) { const [siteResource] = await db .select() .from(siteResources) - .where(and( - eq(siteResources.niceId, niceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) + .where( + and( + eq(siteResources.niceId, niceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) + ) .limit(1); return siteResource; } } -export type GetSiteResourceResponse = NonNullable>>; +export type GetSiteResourceResponse = NonNullable< + Awaited> +>; registry.registerPath({ method: "get", @@ -103,10 +114,7 @@ export async function getSiteResource( if (!siteResource) { return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site resource not found" - ) + createHttpError(HttpCode.NOT_FOUND, "Site resource not found") ); } @@ -119,6 +127,11 @@ export async function getSiteResource( }); } catch (error) { logger.error("Error getting site resource:", error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to get site resource")); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to get site resource" + ) + ); } } diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 5de66505..f6975cd2 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -11,8 +11,8 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; const listAllSiteResourcesByOrgParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listAllSiteResourcesByOrgQuerySchema = z.object({ limit: z @@ -30,7 +30,11 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({ }); export type ListAllSiteResourcesByOrgResponse = { - siteResources: (SiteResource & { siteName: string, siteNiceId: string, siteAddress: string | null })[]; + siteResources: (SiteResource & { + siteName: string; + siteNiceId: string; + siteAddress: string | null; + })[]; }; registry.registerPath({ @@ -51,7 +55,9 @@ export async function listAllSiteResourcesByOrg( next: NextFunction ): Promise { try { - const parsedParams = listAllSiteResourcesByOrgParamsSchema.safeParse(req.params); + const parsedParams = listAllSiteResourcesByOrgParamsSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -61,7 +67,9 @@ export async function listAllSiteResourcesByOrg( ); } - const parsedQuery = listAllSiteResourcesByOrgQuerySchema.safeParse(req.query); + const parsedQuery = listAllSiteResourcesByOrgQuerySchema.safeParse( + req.query + ); if (!parsedQuery.success) { return next( createHttpError( @@ -108,6 +116,11 @@ export async function listAllSiteResourcesByOrg( }); } catch (error) { logger.error("Error listing all site resources by org:", error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to list site resources" + ) + ); } } diff --git a/server/routers/siteResource/listSiteResourceClients.ts b/server/routers/siteResource/listSiteResourceClients.ts index 9b04ac32..772750d1 100644 --- a/server/routers/siteResource/listSiteResourceClients.ts +++ b/server/routers/siteResource/listSiteResourceClients.ts @@ -52,7 +52,9 @@ export async function listSiteResourceClients( next: NextFunction ): Promise { try { - const parsedParams = listSiteResourceClientsSchema.safeParse(req.params); + const parsedParams = listSiteResourceClientsSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -82,4 +84,3 @@ export async function listSiteResourceClients( ); } } - diff --git a/server/routers/siteResource/listSiteResourceRoles.ts b/server/routers/siteResource/listSiteResourceRoles.ts index 5504c003..0dc5913b 100644 --- a/server/routers/siteResource/listSiteResourceRoles.ts +++ b/server/routers/siteResource/listSiteResourceRoles.ts @@ -83,4 +83,3 @@ export async function listSiteResourceRoles( ); } } - diff --git a/server/routers/siteResource/listSiteResourceUsers.ts b/server/routers/siteResource/listSiteResourceUsers.ts index 6cc19557..daf75480 100644 --- a/server/routers/siteResource/listSiteResourceUsers.ts +++ b/server/routers/siteResource/listSiteResourceUsers.ts @@ -86,4 +86,3 @@ export async function listSiteResourceUsers( ); } } - diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts index e530952d..6ecda7c4 100644 --- a/server/routers/siteResource/listSiteResources.ts +++ b/server/routers/siteResource/listSiteResources.ts @@ -11,9 +11,9 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; const listSiteResourcesParamsSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()), - orgId: z.string() - }); + siteId: z.string().transform(Number).pipe(z.int().positive()), + orgId: z.string() +}); const listSiteResourcesQuerySchema = z.object({ limit: z @@ -52,7 +52,9 @@ export async function listSiteResources( next: NextFunction ): Promise { try { - const parsedParams = listSiteResourcesParamsSchema.safeParse(req.params); + const parsedParams = listSiteResourcesParamsSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -83,22 +85,19 @@ export async function listSiteResources( .limit(1); if (site.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site not found" - ) - ); + 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) - )) + .where( + and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) + ) .limit(limit) .offset(offset); @@ -111,6 +110,11 @@ export async function listSiteResources( }); } catch (error) { logger.error("Error listing site resources:", error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to list site resources" + ) + ); } } diff --git a/server/routers/siteResource/removeClientFromSiteResource.ts b/server/routers/siteResource/removeClientFromSiteResource.ts index c6a5dfe8..351128d1 100644 --- a/server/routers/siteResource/removeClientFromSiteResource.ts +++ b/server/routers/siteResource/removeClientFromSiteResource.ts @@ -28,7 +28,8 @@ const removeClientFromSiteResourceParamsSchema = z registry.registerPath({ method: "post", path: "/site-resource/{siteResourceId}/clients/remove", - description: "Remove a single client from a site resource. Clients with a userId cannot be removed.", + description: + "Remove a single client from a site resource. Clients with a userId cannot be removed.", tags: [OpenAPITags.Resource, OpenAPITags.Client], request: { params: removeClientFromSiteResourceParamsSchema, @@ -159,4 +160,3 @@ export async function removeClientFromSiteResource( ); } } - diff --git a/server/routers/siteResource/removeRoleFromSiteResource.ts b/server/routers/siteResource/removeRoleFromSiteResource.ts index 0041ed83..c9857e84 100644 --- a/server/routers/siteResource/removeRoleFromSiteResource.ts +++ b/server/routers/siteResource/removeRoleFromSiteResource.ts @@ -151,7 +151,7 @@ export async function removeRoleFromSiteResource( ) ); - await rebuildClientAssociationsFromSiteResource(siteResource, trx); + await rebuildClientAssociationsFromSiteResource(siteResource, trx); }); return response(res, { @@ -168,4 +168,3 @@ export async function removeRoleFromSiteResource( ); } } - diff --git a/server/routers/siteResource/removeUserFromSiteResource.ts b/server/routers/siteResource/removeUserFromSiteResource.ts index 280a01f2..84347b2f 100644 --- a/server/routers/siteResource/removeUserFromSiteResource.ts +++ b/server/routers/siteResource/removeUserFromSiteResource.ts @@ -138,4 +138,3 @@ export async function removeUserFromSiteResource( ); } } - diff --git a/server/routers/siteResource/setSiteResourceClients.ts b/server/routers/siteResource/setSiteResourceClients.ts index 0a25b7e9..5a8acbcf 100644 --- a/server/routers/siteResource/setSiteResourceClients.ts +++ b/server/routers/siteResource/setSiteResourceClients.ts @@ -62,7 +62,9 @@ export async function setSiteResourceClients( const { clientIds } = parsedBody.data; - const parsedParams = setSiteResourceClientsParamsSchema.safeParse(req.params); + const parsedParams = setSiteResourceClientsParamsSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -95,9 +97,7 @@ export async function setSiteResourceClients( const clientsWithUsers = await db .select() .from(clients) - .where( - inArray(clients.clientId, clientIds) - ); + .where(inArray(clients.clientId, clientIds)); const clientsWithUserId = clientsWithUsers.filter( (client) => client.userId !== null @@ -119,9 +119,12 @@ export async function setSiteResourceClients( .where(eq(clientSiteResources.siteResourceId, siteResourceId)); if (clientIds.length > 0) { - await trx - .insert(clientSiteResources) - .values(clientIds.map((clientId) => ({ clientId, siteResourceId }))); + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); } await rebuildClientAssociationsFromSiteResource(siteResource, trx); @@ -141,4 +144,3 @@ export async function setSiteResourceClients( ); } } - diff --git a/server/routers/siteResource/setSiteResourceRoles.ts b/server/routers/siteResource/setSiteResourceRoles.ts index 7aa07de1..bb71a16b 100644 --- a/server/routers/siteResource/setSiteResourceRoles.ts +++ b/server/routers/siteResource/setSiteResourceRoles.ts @@ -136,15 +136,19 @@ export async function setSiteResourceRoles( ) ); } else { - await trx.delete(roleSiteResources).where( - eq(roleSiteResources.siteResourceId, siteResourceId) - ); + await trx + .delete(roleSiteResources) + .where( + eq(roleSiteResources.siteResourceId, siteResourceId) + ); } if (roleIds.length > 0) { await trx .insert(roleSiteResources) - .values(roleIds.map((roleId) => ({ roleId, siteResourceId }))); + .values( + roleIds.map((roleId) => ({ roleId, siteResourceId })) + ); } await rebuildClientAssociationsFromSiteResource(siteResource, trx); diff --git a/server/routers/siteResource/setSiteResourceUsers.ts b/server/routers/siteResource/setSiteResourceUsers.ts index 4dae0ada..eacd826c 100644 --- a/server/routers/siteResource/setSiteResourceUsers.ts +++ b/server/routers/siteResource/setSiteResourceUsers.ts @@ -63,7 +63,9 @@ export async function setSiteResourceUsers( const { userIds } = parsedBody.data; - const parsedParams = setSiteResourceUsersParamsSchema.safeParse(req.params); + const parsedParams = setSiteResourceUsersParamsSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -99,7 +101,9 @@ export async function setSiteResourceUsers( if (userIds.length > 0) { await trx .insert(userSiteResources) - .values(userIds.map((userId) => ({ userId, siteResourceId }))); + .values( + userIds.map((userId) => ({ userId, siteResourceId })) + ); } await rebuildClientAssociationsFromSiteResource(siteResource, trx); @@ -119,4 +123,3 @@ export async function setSiteResourceUsers( ); } } - diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index f7fda549..efc4939b 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -2,11 +2,13 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { clientSiteResources, + clientSiteResourcesAssociationsCache, db, newts, roles, roleSiteResources, sites, + Transaction, userSiteResources } from "@server/db"; import { siteResources, SiteResource } from "@server/db"; @@ -295,110 +297,16 @@ export async function updateSiteResource( ); } - const { mergedAllClients } = - await rebuildClientAssociationsFromSiteResource( - existingSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below - trx - ); - - // after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed - const destinationChanged = - existingSiteResource.destination !== - updatedSiteResource.destination; - const aliasChanged = - existingSiteResource.alias !== updatedSiteResource.alias; - - if (destinationChanged || aliasChanged) { - const [newt] = await trx - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - if (!newt) { - return next( - createHttpError(HttpCode.NOT_FOUND, "Newt not found") - ); - } - - let oldDestinationStillInUseByASite = false; - // Only update targets on newt if destination changed - if (destinationChanged) { - const oldTargets = generateSubnetProxyTargets( - existingSiteResource, - mergedAllClients - ); - const newTargets = generateSubnetProxyTargets( - updatedSiteResource, - mergedAllClients - ); - - await updateTargets(newt.newtId, { - oldTargets: oldTargets, - newTargets: newTargets - }); - - const oldDestinationStillInUseSites = await trx - .select() - .from(siteResources) - .where( - and( - eq(siteResources.siteId, site.siteId), - eq( - siteResources.destination, - existingSiteResource.destination - ), - ne( - siteResources.siteResourceId, - existingSiteResource.siteResourceId - ) - ) - ); - - oldDestinationStillInUseByASite = - oldDestinationStillInUseSites.length > 0; - } - - const olmJobs: Promise[] = []; - for (const client of mergedAllClients) { - // we also need to update the remote subnets on the olms for each client that has access to this site - olmJobs.push( - updatePeerData( - client.clientId, - updatedSiteResource.siteId, - destinationChanged - ? { - oldRemoteSubnets: - !oldDestinationStillInUseByASite - ? generateRemoteSubnets([ - existingSiteResource - ]) - : [], - newRemoteSubnets: generateRemoteSubnets([ - updatedSiteResource - ]) - } - : undefined, - aliasChanged - ? { - oldAliases: generateAliasConfig([ - existingSiteResource - ]), - newAliases: generateAliasConfig([ - updatedSiteResource - ]) - } - : undefined - ) - ); - } - - await Promise.all(olmJobs); - } - logger.info( `Updated site resource ${siteResourceId} for site ${siteId}` ); + + await handleMessagingForUpdatedSiteResource( + existingSiteResource, + updatedSiteResource!, + { siteId: site.siteId, orgId: site.orgId }, + trx + ); }); return response(res, { @@ -418,3 +326,125 @@ export async function updateSiteResource( ); } } + +export async function handleMessagingForUpdatedSiteResource( + existingSiteResource: SiteResource | undefined, + updatedSiteResource: SiteResource, + site: { siteId: number; orgId: string }, + trx: Transaction +) { + const { mergedAllClients } = + await rebuildClientAssociationsFromSiteResource( + existingSiteResource || updatedSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below + trx + ); + + // after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed + const destinationChanged = + existingSiteResource && + existingSiteResource.destination !== updatedSiteResource.destination; + const aliasChanged = + existingSiteResource && + existingSiteResource.alias !== updatedSiteResource.alias; + + // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all + + if (destinationChanged || aliasChanged) { + const [newt] = await trx + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + throw new Error( + "Newt not found for site during site resource update" + ); + } + + // Only update targets on newt if destination changed + if (destinationChanged) { + const oldTargets = generateSubnetProxyTargets( + existingSiteResource, + mergedAllClients + ); + const newTargets = generateSubnetProxyTargets( + updatedSiteResource, + mergedAllClients + ); + + await updateTargets(newt.newtId, { + oldTargets: oldTargets, + newTargets: newTargets + }); + } + + const olmJobs: Promise[] = []; + for (const client of mergedAllClients) { + // does this client have access to another resource on this site that has the same destination still? if so we dont want to remove it from their olm yet + // todo: optimize this query if needed + const oldDestinationStillInUseSites = await trx + .select() + .from(siteResources) + .innerJoin( + clientSiteResourcesAssociationsCache, + eq( + clientSiteResourcesAssociationsCache.siteResourceId, + siteResources.siteResourceId + ) + ) + .where( + and( + eq( + clientSiteResourcesAssociationsCache.clientId, + client.clientId + ), + eq(siteResources.siteId, site.siteId), + eq( + siteResources.destination, + existingSiteResource.destination + ), + ne( + siteResources.siteResourceId, + existingSiteResource.siteResourceId + ) + ) + ); + + const oldDestinationStillInUseByASite = + oldDestinationStillInUseSites.length > 0; + + // we also need to update the remote subnets on the olms for each client that has access to this site + olmJobs.push( + updatePeerData( + client.clientId, + updatedSiteResource.siteId, + destinationChanged + ? { + oldRemoteSubnets: !oldDestinationStillInUseByASite + ? generateRemoteSubnets([ + existingSiteResource + ]) + : [], + newRemoteSubnets: generateRemoteSubnets([ + updatedSiteResource + ]) + } + : undefined, + aliasChanged + ? { + oldAliases: generateAliasConfig([ + existingSiteResource + ]), + newAliases: generateAliasConfig([ + updatedSiteResource + ]) + } + : undefined + ) + ); + } + + await Promise.all(olmJobs); + } +} diff --git a/server/routers/supporterKey/validateSupporterKey.ts b/server/routers/supporterKey/validateSupporterKey.ts index d8b16421..9ac3c473 100644 --- a/server/routers/supporterKey/validateSupporterKey.ts +++ b/server/routers/supporterKey/validateSupporterKey.ts @@ -10,9 +10,9 @@ import { db } from "@server/db"; import config from "@server/lib/config"; const validateSupporterKeySchema = z.strictObject({ - githubUsername: z.string().nonempty(), - key: z.string().nonempty() - }); + githubUsername: z.string().nonempty(), + key: z.string().nonempty() +}); export type ValidateSupporterKeyResponse = { valid: boolean; diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 2c09b5a6..5d37f617 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -16,51 +16,41 @@ import { isTargetValid } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; const createTargetParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); const createTargetSchema = z.strictObject({ - siteId: z.int().positive(), - ip: z.string().refine(isTargetValid), - method: z.string().optional().nullable(), - port: z.int().min(1).max(65535), - enabled: z.boolean().default(true), - hcEnabled: z.boolean().optional(), - hcPath: z.string().min(1).optional().nullable(), - hcScheme: z.string().optional().nullable(), - hcMode: z.string().optional().nullable(), - hcHostname: z.string().optional().nullable(), - hcPort: z.int().positive().optional().nullable(), - hcInterval: z.int().positive().min(5).optional().nullable(), - hcUnhealthyInterval: z.int() - .positive() - .min(5) - .optional() - .nullable(), - hcTimeout: z.int().positive().min(1).optional().nullable(), - hcHeaders: z - .array(z.strictObject({ name: z.string(), value: z.string() })) - .nullable() - .optional(), - hcFollowRedirects: z.boolean().optional().nullable(), - hcMethod: z.string().min(1).optional().nullable(), - hcStatus: z.int().optional().nullable(), - hcTlsServerName: z.string().optional().nullable(), - path: z.string().optional().nullable(), - pathMatchType: z - .enum(["exact", "prefix", "regex"]) - .optional() - .nullable(), - rewritePath: z.string().optional().nullable(), - rewritePathType: z - .enum(["exact", "prefix", "regex", "stripPrefix"]) - .optional() - .nullable(), - priority: z.int().min(1).max(1000).optional().nullable() - }); + siteId: z.int().positive(), + ip: z.string().refine(isTargetValid), + method: z.string().optional().nullable(), + port: z.int().min(1).max(65535), + enabled: z.boolean().default(true), + hcEnabled: z.boolean().optional(), + hcPath: z.string().min(1).optional().nullable(), + hcScheme: z.string().optional().nullable(), + hcMode: z.string().optional().nullable(), + hcHostname: z.string().optional().nullable(), + hcPort: z.int().positive().optional().nullable(), + hcInterval: z.int().positive().min(5).optional().nullable(), + hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(), + hcTimeout: z.int().positive().min(1).optional().nullable(), + hcHeaders: z + .array(z.strictObject({ name: z.string(), value: z.string() })) + .nullable() + .optional(), + hcFollowRedirects: z.boolean().optional().nullable(), + hcMethod: z.string().min(1).optional().nullable(), + hcStatus: z.int().optional().nullable(), + hcTlsServerName: z.string().optional().nullable(), + path: z.string().optional().nullable(), + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), + rewritePath: z.string().optional().nullable(), + rewritePathType: z + .enum(["exact", "prefix", "regex", "stripPrefix"]) + .optional() + .nullable(), + priority: z.int().min(1).max(1000).optional().nullable() +}); export type CreateTargetResponse = Target & TargetHealthCheck; @@ -159,7 +149,9 @@ export async function createTarget( if (existingTarget) { // log a warning - logger.warn(`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}`); + logger.warn( + `Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}` + ); } let newTarget: Target[] = []; diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index a70b2a1e..606d8635 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -14,8 +14,8 @@ import { getAllowedIps } from "./helpers"; import { OpenAPITags, registry } from "@server/openApi"; const deleteTargetSchema = z.strictObject({ - targetId: z.string().transform(Number).pipe(z.int().positive()) - }); + targetId: z.string().transform(Number).pipe(z.int().positive()) +}); registry.registerPath({ method: "delete", diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 7fe2e062..749e1399 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -11,12 +11,13 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const getTargetSchema = z.strictObject({ - targetId: z.string().transform(Number).pipe(z.int().positive()) - }); + targetId: z.string().transform(Number).pipe(z.int().positive()) +}); -type GetTargetResponse = Target & Omit & { - hcHeaders: { name: string; value: string; }[] | null; -}; +type GetTargetResponse = Target & + Omit & { + hcHeaders: { name: string; value: string }[] | null; + }; registry.registerPath({ method: "get", diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts index ee4e7950..2bfcff19 100644 --- a/server/routers/target/handleHealthcheckStatusMessage.ts +++ b/server/routers/target/handleHealthcheckStatusMessage.ts @@ -30,7 +30,9 @@ interface HealthcheckStatusMessage { targets: Record; } -export const handleHealthcheckStatusMessage: MessageHandler = async (context) => { +export const handleHealthcheckStatusMessage: MessageHandler = async ( + context +) => { const { message, client: c } = context; const newt = c as Newt; @@ -59,7 +61,9 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) => // Process each target status update for (const [targetId, healthStatus] of Object.entries(data.targets)) { - logger.debug(`Processing health status for target ${targetId}: ${healthStatus.status}${healthStatus.lastError ? ` (${healthStatus.lastError})` : ''}`); + logger.debug( + `Processing health status for target ${targetId}: ${healthStatus.status}${healthStatus.lastError ? ` (${healthStatus.lastError})` : ""}` + ); // Verify the target belongs to this newt's site before updating // This prevents unauthorized updates to targets from other sites @@ -76,7 +80,10 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) => siteId: targets.siteId }) .from(targets) - .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .innerJoin( + resources, + eq(targets.resourceId, resources.resourceId) + ) .innerJoin(sites, eq(targets.siteId, sites.siteId)) .where( and( @@ -87,7 +94,9 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) => .limit(1); if (!targetCheck) { - logger.warn(`Target ${targetId} not found or does not belong to site ${newt.siteId}`); + logger.warn( + `Target ${targetId} not found or does not belong to site ${newt.siteId}` + ); errorCount++; continue; } @@ -101,11 +110,15 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (context) => .where(eq(targetHealthCheck.targetId, targetIdNum)) .execute(); - logger.debug(`Updated health status for target ${targetId} to ${healthStatus.status}`); + logger.debug( + `Updated health status for target ${targetId} to ${healthStatus.status}` + ); successCount++; } - logger.debug(`Health status update complete: ${successCount} successful, ${errorCount} errors out of ${Object.keys(data.targets).length} targets`); + logger.debug( + `Health status update complete: ${successCount} successful, ${errorCount} errors out of ${Object.keys(data.targets).length} targets` + ); } catch (error) { logger.error("Error processing healthcheck status message:", error); } diff --git a/server/routers/target/helpers.ts b/server/routers/target/helpers.ts index 13b2ee46..fe76bd13 100644 --- a/server/routers/target/helpers.ts +++ b/server/routers/target/helpers.ts @@ -4,7 +4,10 @@ import { eq } from "drizzle-orm"; const currentBannedPorts: number[] = []; -export async function pickPort(siteId: number, trx: Transaction | typeof db): Promise<{ +export async function pickPort( + siteId: number, + trx: Transaction | typeof db +): Promise<{ internalPort: number; targetIps: string[]; }> { diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 356276cb..11a23f02 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -11,11 +11,8 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; const listTargetsParamsSchema = z.strictObject({ - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); const listTargetsSchema = z.object({ limit: z @@ -62,7 +59,7 @@ function queryTargets(resourceId: number) { pathMatchType: targets.pathMatchType, rewritePath: targets.rewritePath, rewritePathType: targets.rewritePathType, - priority: targets.priority, + priority: targets.priority }) .from(targets) .leftJoin(sites, eq(sites.siteId, targets.siteId)) @@ -75,8 +72,11 @@ function queryTargets(resourceId: number) { return baseQuery; } -type TargetWithParsedHeaders = Omit>[0], 'hcHeaders'> & { - hcHeaders: { name: string; value: string; }[] | null; +type TargetWithParsedHeaders = Omit< + Awaited>[0], + "hcHeaders" +> & { + hcHeaders: { name: string; value: string }[] | null; }; export type ListTargetsResponse = { @@ -136,7 +136,7 @@ export async function listTargets( const totalCount = totalCountResult[0].count; // Parse hcHeaders from JSON string back to array for each target - const parsedTargetsList = targetsList.map(target => { + const parsedTargetsList = targetsList.map((target) => { let parsedHcHeaders = null; if (target.hcHeaders) { try { diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 4a60e6cf..b00340ee 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -16,10 +16,11 @@ import { OpenAPITags, registry } from "@server/openApi"; import { vs } from "@react-email/components"; const updateTargetParamsSchema = z.strictObject({ - targetId: z.string().transform(Number).pipe(z.int().positive()) - }); + targetId: z.string().transform(Number).pipe(z.int().positive()) +}); -const updateTargetBodySchema = z.strictObject({ +const updateTargetBodySchema = z + .strictObject({ siteId: z.int().positive(), ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), @@ -32,22 +33,27 @@ const updateTargetBodySchema = z.strictObject({ hcHostname: z.string().optional().nullable(), hcPort: z.int().positive().optional().nullable(), hcInterval: z.int().positive().min(5).optional().nullable(), - hcUnhealthyInterval: z.int() - .positive() - .min(5) - .optional() - .nullable(), + hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(), hcTimeout: z.int().positive().min(1).optional().nullable(), - hcHeaders: z.array(z.strictObject({ name: z.string(), value: z.string() })).nullable().optional(), + hcHeaders: z + .array(z.strictObject({ name: z.string(), value: z.string() })) + .nullable() + .optional(), hcFollowRedirects: z.boolean().optional().nullable(), hcMethod: z.string().min(1).optional().nullable(), hcStatus: z.int().optional().nullable(), hcTlsServerName: z.string().optional().nullable(), path: z.string().optional().nullable(), - pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), + pathMatchType: z + .enum(["exact", "prefix", "regex"]) + .optional() + .nullable(), rewritePath: z.string().optional().nullable(), - rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), - priority: z.int().min(1).max(1000).optional(), + rewritePathType: z + .enum(["exact", "prefix", "regex", "stripPrefix"]) + .optional() + .nullable(), + priority: z.int().min(1).max(1000).optional() }) .refine((data) => Object.keys(data).length > 0, { error: "At least one field must be provided for update" @@ -166,7 +172,9 @@ export async function updateTarget( if (foundTarget) { // log a warning - logger.warn(`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${target.resourceId}`); + logger.warn( + `Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${target.resourceId}` + ); } const { internalPort, targetIps } = await pickPort(site.siteId!, db); @@ -203,6 +211,14 @@ export async function updateTarget( hcHeaders = JSON.stringify(parsedBody.data.hcHeaders); } + // When health check is disabled, reset hcHealth to "unknown" + // to prevent previously unhealthy targets from being excluded + const hcHealthValue = + parsedBody.data.hcEnabled === false || + parsedBody.data.hcEnabled === null + ? "unknown" + : undefined; + const [updatedHc] = await db .update(targetHealthCheck) .set({ @@ -220,6 +236,7 @@ export async function updateTarget( hcMethod: parsedBody.data.hcMethod, hcStatus: parsedBody.data.hcStatus, hcTlsServerName: parsedBody.data.hcTlsServerName, + ...(hcHealthValue !== undefined && { hcHealth: hcHealthValue }) }) .where(eq(targetHealthCheck.targetId, targetId)) .returning(); diff --git a/server/routers/traefik/index.ts b/server/routers/traefik/index.ts index 6f5bd4f0..195f0087 100644 --- a/server/routers/traefik/index.ts +++ b/server/routers/traefik/index.ts @@ -1 +1 @@ -export * from "./traefikConfigProvider"; \ No newline at end of file +export * from "./traefikConfigProvider"; diff --git a/server/routers/traefik/traefikConfigProvider.ts b/server/routers/traefik/traefikConfigProvider.ts index 9b12ed8a..e8ac1621 100644 --- a/server/routers/traefik/traefikConfigProvider.ts +++ b/server/routers/traefik/traefikConfigProvider.ts @@ -59,4 +59,4 @@ export async function traefikConfigProvider( error: "Failed to build Traefik config" }); } -} \ No newline at end of file +} diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts index 3e94d96c..d64ccfb5 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -15,9 +15,9 @@ import { FeatureId } from "@server/lib/billing"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; const acceptInviteBodySchema = z.strictObject({ - token: z.string(), - inviteId: z.string() - }); + token: z.string(), + inviteId: z.string() +}); export type AcceptInviteResponse = { accepted: boolean; diff --git a/server/routers/user/addUserAction.ts b/server/routers/user/addUserAction.ts index f75d5005..ddbae6b0 100644 --- a/server/routers/user/addUserAction.ts +++ b/server/routers/user/addUserAction.ts @@ -10,10 +10,10 @@ import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; const addUserActionSchema = z.strictObject({ - userId: z.string(), - actionId: z.string(), - orgId: z.string() - }); + userId: z.string(), + actionId: z.string(), + orgId: z.string() +}); export async function addUserAction( req: Request, diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts index 38ef264c..ffb9f1ba 100644 --- a/server/routers/user/addUserSite.ts +++ b/server/routers/user/addUserSite.ts @@ -10,9 +10,9 @@ import { eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; const addUserSiteSchema = z.strictObject({ - userId: z.string(), - siteId: z.string().transform(Number).pipe(z.int().positive()) - }); + userId: z.string(), + siteId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function addUserSite( req: Request, @@ -61,7 +61,6 @@ export async function addUserSite( status: HttpCode.CREATED }); }); - } catch (error) { logger.error(error); return next( diff --git a/server/routers/user/adminGeneratePasswordResetCode.ts b/server/routers/user/adminGeneratePasswordResetCode.ts index 5d283c5c..562a459e 100644 --- a/server/routers/user/adminGeneratePasswordResetCode.ts +++ b/server/routers/user/adminGeneratePasswordResetCode.ts @@ -19,7 +19,9 @@ const adminGeneratePasswordResetCodeSchema = z.strictObject({ userId: z.string().min(1) }); -export type AdminGeneratePasswordResetCodeBody = z.infer; +export type AdminGeneratePasswordResetCodeBody = z.infer< + typeof adminGeneratePasswordResetCodeSchema +>; export type AdminGeneratePasswordResetCodeResponse = { token: string; @@ -32,7 +34,9 @@ export async function adminGeneratePasswordResetCode( res: Response, next: NextFunction ): Promise { - const parsedParams = adminGeneratePasswordResetCodeSchema.safeParse(req.params); + const parsedParams = adminGeneratePasswordResetCodeSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( @@ -52,12 +56,7 @@ export async function adminGeneratePasswordResetCode( .where(eq(users.userId, userId)); if (!existingUser || !existingUser.length) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "User not found" - ) - ); + return next(createHttpError(HttpCode.NOT_FOUND, "User not found")); } if (existingUser[0].type !== UserType.Internal) { @@ -122,4 +121,3 @@ export async function adminGeneratePasswordResetCode( ); } } - diff --git a/server/routers/user/adminGetUser.ts b/server/routers/user/adminGetUser.ts index bda14476..06045c77 100644 --- a/server/routers/user/adminGetUser.ts +++ b/server/routers/user/adminGetUser.ts @@ -10,8 +10,8 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; const adminGetUserSchema = z.strictObject({ - userId: z.string().min(1) - }); + userId: z.string().min(1) +}); registry.registerPath({ method: "get", diff --git a/server/routers/user/adminListUsers.ts b/server/routers/user/adminListUsers.ts index a3ad9cdd..3a965259 100644 --- a/server/routers/user/adminListUsers.ts +++ b/server/routers/user/adminListUsers.ts @@ -10,19 +10,19 @@ import { idp, users } from "@server/db"; import { fromZodError } from "zod-validation-error"; const listUsersSchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function queryUsers(limit: number, offset: number) { return await db diff --git a/server/routers/user/adminUpdateUser2FA.ts b/server/routers/user/adminUpdateUser2FA.ts index 4bb2486a..7fb37d01 100644 --- a/server/routers/user/adminUpdateUser2FA.ts +++ b/server/routers/user/adminUpdateUser2FA.ts @@ -11,12 +11,12 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const updateUser2FAParamsSchema = z.strictObject({ - userId: z.string() - }); + userId: z.string() +}); const updateUser2FABodySchema = z.strictObject({ - twoFactorSetupRequested: z.boolean() - }); + twoFactorSetupRequested: z.boolean() +}); export type UpdateUser2FAResponse = { userId: string; @@ -90,13 +90,15 @@ export async function updateUser2FA( ); } - logger.debug(`Updating 2FA for user ${userId} to ${twoFactorSetupRequested}`); + logger.debug( + `Updating 2FA for user ${userId} to ${twoFactorSetupRequested}` + ); if (twoFactorSetupRequested) { await db .update(users) .set({ - twoFactorSetupRequested: true, + twoFactorSetupRequested: true }) .where(eq(users.userId, userId)); } else { diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index 99a2258c..e1902477 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -18,25 +18,26 @@ import { TierId } from "@server/lib/billing/tiers"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; const paramsSchema = z.strictObject({ - orgId: z.string().nonempty() - }); + orgId: z.string().nonempty() +}); const bodySchema = z.strictObject({ - email: z.email() - .toLowerCase() - .optional() - .refine((data) => { - if (data) { - return z.email().safeParse(data).success; - } - return true; - }), - username: z.string().nonempty().toLowerCase(), - name: z.string().optional(), - type: z.enum(["internal", "oidc"]).optional(), - idpId: z.number().optional(), - roleId: z.number() - }); + email: z + .email() + .toLowerCase() + .optional() + .refine((data) => { + if (data) { + return z.email().safeParse(data).success; + } + return true; + }), + username: z.string().nonempty().toLowerCase(), + name: z.string().optional(), + type: z.enum(["internal", "oidc"]).optional(), + idpId: z.number().optional(), + roleId: z.number() +}); export type CreateOrgUserResponse = {}; diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 4e09afd6..f22a29d3 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -47,9 +47,9 @@ export type GetOrgUserResponse = NonNullable< >; const getOrgUserParamsSchema = z.strictObject({ - userId: z.string(), - orgId: z.string() - }); + userId: z.string(), + orgId: z.string() +}); registry.registerPath({ method: "get", diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index f43ebeb8..6a778868 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -22,16 +22,16 @@ import { build } from "@server/build"; import cache from "@server/lib/cache"; const inviteUserParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const inviteUserBodySchema = z.strictObject({ - email: z.email().toLowerCase(), - roleId: z.number(), - validHours: z.number().gt(0).lte(168), - sendEmail: z.boolean().optional(), - regenerate: z.boolean().optional() - }); + email: z.email().toLowerCase(), + roleId: z.number(), + validHours: z.number().gt(0).lte(168), + sendEmail: z.boolean().optional(), + regenerate: z.boolean().optional() +}); export type InviteUserBody = z.infer; @@ -109,12 +109,7 @@ export async function inviteUser( const [role] = await db .select() .from(roles) - .where( - and( - eq(roles.roleId, roleId), - eq(roles.orgId, orgId) - ) - ) + .where(and(eq(roles.roleId, roleId), eq(roles.orgId, orgId))) .limit(1); if (!role) { diff --git a/server/routers/user/listInvitations.ts b/server/routers/user/listInvitations.ts index a61e2372..4289b877 100644 --- a/server/routers/user/listInvitations.ts +++ b/server/routers/user/listInvitations.ts @@ -11,23 +11,23 @@ import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const listInvitationsParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listInvitationsQuerySchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function queryInvitations(orgId: string, limit: number, offset: number) { return await db diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index aa70874e..401dcf58 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -12,23 +12,23 @@ import { OpenAPITags, registry } from "@server/openApi"; import { eq } from "drizzle-orm"; const listUsersParamsSchema = z.strictObject({ - orgId: z.string() - }); + orgId: z.string() +}); const listUsersSchema = z.strictObject({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.int().nonnegative()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()) - }); + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); async function queryUsers(orgId: string, limit: number, offset: number) { return await db @@ -48,7 +48,7 @@ async function queryUsers(orgId: string, limit: number, offset: number) { idpId: users.idpId, idpType: idp.type, idpVariant: idpOidcConfig.variant, - twoFactorEnabled: users.twoFactorEnabled, + twoFactorEnabled: users.twoFactorEnabled }) .from(users) .leftJoin(userOrgs, eq(users.userId, userOrgs.userId)) diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index 44ec8c23..6a000afc 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -10,9 +10,9 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const removeInvitationParamsSchema = z.strictObject({ - orgId: z.string(), - inviteId: z.string() - }); + orgId: z.string(), + inviteId: z.string() +}); export async function removeInvitation( req: Request, diff --git a/server/routers/user/removeUserAction.ts b/server/routers/user/removeUserAction.ts index 6e4c1a66..b9dc8cc0 100644 --- a/server/routers/user/removeUserAction.ts +++ b/server/routers/user/removeUserAction.ts @@ -10,13 +10,13 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const removeUserActionParamsSchema = z.strictObject({ - userId: z.string() - }); + userId: z.string() +}); const removeUserActionSchema = z.strictObject({ - actionId: z.string(), - orgId: z.string() - }); + actionId: z.string(), + orgId: z.string() +}); export async function removeUserAction( req: Request, diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index cbbb4495..97045e92 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -16,9 +16,9 @@ import { UserType } from "@server/types/UserTypes"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; const removeUserSchema = z.strictObject({ - userId: z.string(), - orgId: z.string() - }); + userId: z.string(), + orgId: z.string() +}); registry.registerPath({ method: "delete", diff --git a/server/routers/user/removeUserResource.ts b/server/routers/user/removeUserResource.ts index 14dbb540..bdb0cda3 100644 --- a/server/routers/user/removeUserResource.ts +++ b/server/routers/user/removeUserResource.ts @@ -10,12 +10,9 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const removeUserResourceSchema = z.strictObject({ - userId: z.string(), - resourceId: z - .string() - .transform(Number) - .pipe(z.int().positive()) - }); + userId: z.string(), + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); export async function removeUserResource( req: Request, diff --git a/server/routers/user/removeUserSite.ts b/server/routers/user/removeUserSite.ts index 6ed2288a..a531f02c 100644 --- a/server/routers/user/removeUserSite.ts +++ b/server/routers/user/removeUserSite.ts @@ -10,12 +10,12 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; const removeUserSiteParamsSchema = z.strictObject({ - userId: z.string() - }); + userId: z.string() +}); const removeUserSiteSchema = z.strictObject({ - siteId: z.int().positive() - }); + siteId: z.int().positive() +}); export async function removeUserSite( req: Request, diff --git a/server/routers/user/updateOrgUser.ts b/server/routers/user/updateOrgUser.ts index e1000063..97bedb5f 100644 --- a/server/routers/user/updateOrgUser.ts +++ b/server/routers/user/updateOrgUser.ts @@ -10,11 +10,12 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.strictObject({ - userId: z.string(), - orgId: z.string() - }); + userId: z.string(), + orgId: z.string() +}); -const bodySchema = z.strictObject({ +const bodySchema = z + .strictObject({ autoProvisioned: z.boolean().optional() }) .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/ws/index.ts b/server/routers/ws/index.ts index 16440ec9..b580b369 100644 --- a/server/routers/ws/index.ts +++ b/server/routers/ws/index.ts @@ -1,2 +1,2 @@ export * from "./ws"; -export * from "./types"; \ No newline at end of file +export * from "./types"; diff --git a/server/routers/ws/types.ts b/server/routers/ws/types.ts index 7063bc87..b4ec690b 100644 --- a/server/routers/ws/types.ts +++ b/server/routers/ws/types.ts @@ -58,7 +58,9 @@ export interface HandlerContext { connectedClients: Map; } -export type MessageHandler = (context: HandlerContext) => Promise; +export type MessageHandler = ( + context: HandlerContext +) => Promise; // Redis message type for cross-node communication export interface RedisMessage { @@ -67,4 +69,4 @@ export interface RedisMessage { excludeClientId?: string; message: WSMessage; fromNodeId: string; -} \ No newline at end of file +} diff --git a/server/routers/ws/ws.ts b/server/routers/ws/ws.ts index abbec880..0544af9d 100644 --- a/server/routers/ws/ws.ts +++ b/server/routers/ws/ws.ts @@ -10,7 +10,13 @@ import { validateOlmSessionToken } from "@server/auth/sessions/olm"; import { messageHandlers } from "./messageHandlers"; import logger from "@server/logger"; import { v4 as uuidv4 } from "uuid"; -import { ClientType, TokenPayload, WebSocketRequest, WSMessage, AuthenticatedWebSocket } from "./types"; +import { + ClientType, + TokenPayload, + WebSocketRequest, + WSMessage, + AuthenticatedWebSocket +} from "./types"; import { validateSessionToken } from "@server/auth/sessions/app"; // Subset of TokenPayload for public ws.ts (newt and olm only) @@ -32,7 +38,11 @@ const connectedClients: Map = new Map(); const getClientMapKey = (clientId: string) => clientId; // Helper functions for client management -const addClient = async (clientType: ClientType, clientId: string, ws: AuthenticatedWebSocket): Promise => { +const addClient = async ( + clientType: ClientType, + clientId: string, + ws: AuthenticatedWebSocket +): Promise => { // Generate unique connection ID const connectionId = uuidv4(); ws.connectionId = connectionId; @@ -43,33 +53,46 @@ const addClient = async (clientType: ClientType, clientId: string, ws: Authentic existingClients.push(ws); connectedClients.set(mapKey, existingClients); - logger.info(`Client added to tracking - ${clientType.toUpperCase()} ID: ${clientId}, Connection ID: ${connectionId}, Total connections: ${existingClients.length}`); + logger.info( + `Client added to tracking - ${clientType.toUpperCase()} ID: ${clientId}, Connection ID: ${connectionId}, Total connections: ${existingClients.length}` + ); }; -const removeClient = async (clientType: ClientType, clientId: string, ws: AuthenticatedWebSocket): Promise => { +const removeClient = async ( + clientType: ClientType, + clientId: string, + ws: AuthenticatedWebSocket +): Promise => { const mapKey = getClientMapKey(clientId); const existingClients = connectedClients.get(mapKey) || []; - const updatedClients = existingClients.filter(client => client !== ws); + const updatedClients = existingClients.filter((client) => client !== ws); if (updatedClients.length === 0) { connectedClients.delete(mapKey); - logger.info(`All connections removed for ${clientType.toUpperCase()} ID: ${clientId}`); + logger.info( + `All connections removed for ${clientType.toUpperCase()} ID: ${clientId}` + ); } else { connectedClients.set(mapKey, updatedClients); - logger.info(`Connection removed - ${clientType.toUpperCase()} ID: ${clientId}, Remaining connections: ${updatedClients.length}`); + logger.info( + `Connection removed - ${clientType.toUpperCase()} ID: ${clientId}, Remaining connections: ${updatedClients.length}` + ); } }; // Local message sending (within this node) -const sendToClientLocal = async (clientId: string, message: WSMessage): Promise => { +const sendToClientLocal = async ( + clientId: string, + message: WSMessage +): Promise => { const mapKey = getClientMapKey(clientId); const clients = connectedClients.get(mapKey); if (!clients || clients.length === 0) { return false; } const messageString = JSON.stringify(message); - clients.forEach(client => { + clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(messageString); } @@ -77,11 +100,14 @@ const sendToClientLocal = async (clientId: string, message: WSMessage): Promise< return true; }; -const broadcastToAllExceptLocal = async (message: WSMessage, excludeClientId?: string): Promise => { +const broadcastToAllExceptLocal = async ( + message: WSMessage, + excludeClientId?: string +): Promise => { connectedClients.forEach((clients, mapKey) => { const [type, id] = mapKey.split(":"); if (!(excludeClientId && id === excludeClientId)) { - clients.forEach(client => { + clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); } @@ -91,39 +117,53 @@ const broadcastToAllExceptLocal = async (message: WSMessage, excludeClientId?: s }; // Cross-node message sending -const sendToClient = async (clientId: string, message: WSMessage): Promise => { +const sendToClient = async ( + clientId: string, + message: WSMessage +): Promise => { // Try to send locally first const localSent = await sendToClientLocal(clientId, message); - logger.debug(`sendToClient: Message type ${message.type} sent to clientId ${clientId}`); + logger.debug( + `sendToClient: Message type ${message.type} sent to clientId ${clientId}` + ); return localSent; }; -const broadcastToAllExcept = async (message: WSMessage, excludeClientId?: string): Promise => { +const broadcastToAllExcept = async ( + message: WSMessage, + excludeClientId?: string +): Promise => { // Broadcast locally await broadcastToAllExceptLocal(message, excludeClientId); }; // Check if a client has active connections across all nodes const hasActiveConnections = async (clientId: string): Promise => { - const mapKey = getClientMapKey(clientId); - const clients = connectedClients.get(mapKey); - return !!(clients && clients.length > 0); + const mapKey = getClientMapKey(clientId); + const clients = connectedClients.get(mapKey); + return !!(clients && clients.length > 0); }; // Get all active nodes for a client -const getActiveNodes = async (clientType: ClientType, clientId: string): Promise => { - const mapKey = getClientMapKey(clientId); - const clients = connectedClients.get(mapKey); - return (clients && clients.length > 0) ? [NODE_ID] : []; +const getActiveNodes = async ( + clientType: ClientType, + clientId: string +): Promise => { + const mapKey = getClientMapKey(clientId); + const clients = connectedClients.get(mapKey); + return clients && clients.length > 0 ? [NODE_ID] : []; }; // Token verification middleware -const verifyToken = async (token: string, clientType: ClientType, userToken: string): Promise => { - -try { - if (clientType === 'newt') { +const verifyToken = async ( + token: string, + clientType: ClientType, + userToken: string +): Promise => { + try { + if (clientType === "newt") { const { session, newt } = await validateNewtSessionToken(token); if (!session || !newt) { return null; @@ -136,7 +176,7 @@ try { return null; } return { client: existingNewt[0], session, clientType }; - } else if (clientType === 'olm') { + } else if (clientType === "olm") { const { session, olm } = await validateOlmSessionToken(token); if (!session || !olm) { return null; @@ -149,8 +189,10 @@ try { return null; } - if (olm.userId) { // this is a user device and we need to check the user token - const { session: userSession, user } = await validateSessionToken(userToken); + if (olm.userId) { + // this is a user device and we need to check the user token + const { session: userSession, user } = + await validateSessionToken(userToken); if (!userSession || !user) { return null; } @@ -161,7 +203,7 @@ try { return { client: existingOlm[0], session, clientType }; } - + return null; } catch (error) { logger.error("Token verification failed:", error); @@ -169,7 +211,11 @@ try { } }; -const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, clientType: "newt" | "olm"): Promise => { +const setupConnection = async ( + ws: AuthenticatedWebSocket, + client: Newt | Olm, + clientType: "newt" | "olm" +): Promise => { logger.info("Establishing websocket connection"); if (!client) { logger.error("Connection attempt without client"); @@ -180,7 +226,8 @@ const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, c ws.clientType = clientType; // Add client to tracking - const clientId = clientType === 'newt' ? (client as Newt).newtId : (client as Olm).olmId; + const clientId = + clientType === "newt" ? (client as Newt).newtId : (client as Olm).olmId; await addClient(clientType, clientId, ws); ws.on("message", async (data) => { @@ -188,7 +235,9 @@ const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, c const message: WSMessage = JSON.parse(data.toString()); if (!message.type || typeof message.type !== "string") { - throw new Error("Invalid message format: missing or invalid type"); + throw new Error( + "Invalid message format: missing or invalid type" + ); } const handler = messageHandlers[message.type]; @@ -213,33 +262,48 @@ const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, c response.excludeSender ? clientId : undefined ); } else if (response.targetClientId) { - await sendToClient(response.targetClientId, response.message); + await sendToClient( + response.targetClientId, + response.message + ); } else { ws.send(JSON.stringify(response.message)); } } } catch (error) { logger.error("Message handling error:", error); - ws.send(JSON.stringify({ - type: "error", - data: { - message: error instanceof Error ? error.message : "Unknown error occurred", - originalMessage: data.toString() - } - })); + ws.send( + JSON.stringify({ + type: "error", + data: { + message: + error instanceof Error + ? error.message + : "Unknown error occurred", + originalMessage: data.toString() + } + }) + ); } }); ws.on("close", () => { removeClient(clientType, clientId, ws); - logger.info(`Client disconnected - ${clientType.toUpperCase()} ID: ${clientId}`); + logger.info( + `Client disconnected - ${clientType.toUpperCase()} ID: ${clientId}` + ); }); ws.on("error", (error: Error) => { - logger.error(`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`, error); + logger.error( + `WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`, + error + ); }); - logger.info(`WebSocket connection established - ${clientType.toUpperCase()} ID: ${clientId}`); + logger.info( + `WebSocket connection established - ${clientType.toUpperCase()} ID: ${clientId}` + ); }; // Router endpoint @@ -249,55 +313,89 @@ router.get("/ws", (req: Request, res: Response) => { // WebSocket upgrade handler const handleWSUpgrade = (server: HttpServer): void => { - server.on("upgrade", async (request: WebSocketRequest, socket: Socket, head: Buffer) => { - try { - const url = new URL(request.url || '', `http://${request.headers.host}`); - const token = url.searchParams.get('token') || request.headers["sec-websocket-protocol"] || ''; - const userToken = url.searchParams.get('userToken') || ''; - let clientType = url.searchParams.get('clientType') as ClientType; + server.on( + "upgrade", + async (request: WebSocketRequest, socket: Socket, head: Buffer) => { + try { + const url = new URL( + request.url || "", + `http://${request.headers.host}` + ); + const token = + url.searchParams.get("token") || + request.headers["sec-websocket-protocol"] || + ""; + const userToken = url.searchParams.get("userToken") || ""; + let clientType = url.searchParams.get( + "clientType" + ) as ClientType; - if (!clientType) { - clientType = "newt"; - } + if (!clientType) { + clientType = "newt"; + } - if (!token || !clientType || !['newt', 'olm'].includes(clientType)) { - logger.warn("Unauthorized connection attempt: invalid token or client type..."); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + if ( + !token || + !clientType || + !["newt", "olm"].includes(clientType) + ) { + logger.warn( + "Unauthorized connection attempt: invalid token or client type..." + ); + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.destroy(); + return; + } + + const tokenPayload = await verifyToken( + token, + clientType, + userToken + ); + if (!tokenPayload) { + logger.warn( + "Unauthorized connection attempt: invalid token..." + ); + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.destroy(); + return; + } + + wss.handleUpgrade( + request, + socket, + head, + (ws: AuthenticatedWebSocket) => { + setupConnection( + ws, + tokenPayload.client, + tokenPayload.clientType + ); + } + ); + } catch (error) { + logger.error("WebSocket upgrade error:", error); + socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n"); socket.destroy(); - return; } - - const tokenPayload = await verifyToken(token, clientType, userToken); - if (!tokenPayload) { - logger.warn("Unauthorized connection attempt: invalid token..."); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - - wss.handleUpgrade(request, socket, head, (ws: AuthenticatedWebSocket) => { - setupConnection(ws, tokenPayload.client, tokenPayload.clientType); - }); - } catch (error) { - logger.error("WebSocket upgrade error:", error); - socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n"); - socket.destroy(); } - }); + ); }; // Disconnect a specific client and force them to reconnect const disconnectClient = async (clientId: string): Promise => { const mapKey = getClientMapKey(clientId); const clients = connectedClients.get(mapKey); - + if (!clients || clients.length === 0) { logger.debug(`No connections found for client ID: ${clientId}`); return false; } - logger.info(`Disconnecting client ID: ${clientId} (${clients.length} connection(s))`); - + logger.info( + `Disconnecting client ID: ${clientId} (${clients.length} connection(s))` + ); + // Close all connections for this client clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { @@ -313,16 +411,16 @@ const cleanup = async (): Promise => { try { // Close all WebSocket connections connectedClients.forEach((clients) => { - clients.forEach(client => { + clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.terminate(); } }); }); - logger.info('WebSocket cleanup completed'); + logger.info("WebSocket cleanup completed"); } catch (error) { - logger.error('Error during WebSocket cleanup:', error); + logger.error("Error during WebSocket cleanup:", error); } }; diff --git a/server/setup/clearStaleData.ts b/server/setup/clearStaleData.ts index 2e54656c..8c7e85f0 100644 --- a/server/setup/clearStaleData.ts +++ b/server/setup/clearStaleData.ts @@ -1,5 +1,5 @@ import { build } from "@server/build"; -import { db, sessionTransferToken } from "@server/db"; +import { db, deviceWebAuthCodes, sessionTransferToken } from "@server/db"; import { emailVerificationCodes, newtSessions, @@ -89,4 +89,12 @@ export async function clearStaleData() { logger.warn("Error clearing expired sessionTransferToken:", e); } } + + try { + await db + .delete(deviceWebAuthCodes) + .where(lt(deviceWebAuthCodes.expiresAt, new Date().getTime())); + } catch (e) { + logger.warn("Error clearing expired deviceWebAuthCodes:", e); + } } diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 1734b5e6..64298029 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -31,7 +31,9 @@ export async function ensureSetupToken() { // If admin exists, no need for setup token if (existingAdmin) { - logger.warn("Server admin exists. Setup token generation skipped."); + logger.debug( + "Server admin exists. Setup token generation skipped." + ); return; } @@ -70,4 +72,4 @@ export async function ensureSetupToken() { console.error("Failed to ensure setup token:", error); throw error; } -} \ No newline at end of file +} diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index f3b07bec..0fc42f9d 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -14,6 +14,7 @@ import m6 from "./scriptsPg/1.10.2"; import m7 from "./scriptsPg/1.11.0"; import m8 from "./scriptsPg/1.11.1"; import m9 from "./scriptsPg/1.12.0"; +import m10 from "./scriptsPg/1.13.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -28,7 +29,8 @@ const migrations = [ { version: "1.10.2", run: m6 }, { version: "1.11.0", run: m7 }, { version: "1.11.1", run: m8 }, - { version: "1.12.0", run: m9 } + { version: "1.12.0", run: m9 }, + { version: "1.13.0", run: m10 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index dd546db2..8ff66c49 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -32,6 +32,7 @@ import m27 from "./scriptsSqlite/1.10.2"; import m28 from "./scriptsSqlite/1.11.0"; import m29 from "./scriptsSqlite/1.11.1"; import m30 from "./scriptsSqlite/1.12.0"; +import m31 from "./scriptsSqlite/1.13.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -62,7 +63,8 @@ const migrations = [ { version: "1.10.2", run: m27 }, { version: "1.11.0", run: m28 }, { version: "1.11.1", run: m29 }, - { version: "1.12.0", run: m30 } + { version: "1.12.0", run: m30 }, + { version: "1.13.0", run: m31 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.12.0.ts b/server/setup/scriptsPg/1.12.0.ts index 38cdaf43..d3c257e3 100644 --- a/server/setup/scriptsPg/1.12.0.ts +++ b/server/setup/scriptsPg/1.12.0.ts @@ -9,7 +9,9 @@ export default async function migration() { try { await db.execute(sql`BEGIN`); - await db.execute(sql`UPDATE "resourceRules" SET "match" = 'COUNTRY' WHERE "match" = 'GEOIP'`); + await db.execute( + sql`UPDATE "resourceRules" SET "match" = 'COUNTRY' WHERE "match" = 'GEOIP'` + ); await db.execute(sql` CREATE TABLE "accessAuditLog" ( @@ -92,40 +94,97 @@ export default async function migration() { ); `); - await db.execute(sql`ALTER TABLE "blueprints" ADD CONSTRAINT "blueprints_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "blueprints" ADD CONSTRAINT "blueprints_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "remoteExitNode" ADD COLUMN "secondaryVersion" varchar;`); - await db.execute(sql`ALTER TABLE "resources" DROP CONSTRAINT "resources_skipToIdpId_idp_idpId_fk";`); - await db.execute(sql`ALTER TABLE "domains" ADD COLUMN "certResolver" varchar;`); - await db.execute(sql`ALTER TABLE "domains" ADD COLUMN "customCertResolver" varchar;`); - await db.execute(sql`ALTER TABLE "domains" ADD COLUMN "preferWildcardCert" boolean;`); - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "requireTwoFactor" boolean;`); - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "maxSessionLengthHours" integer;`); - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "passwordExpiryDays" integer;`); - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysRequest" integer DEFAULT 7 NOT NULL;`); - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysAccess" integer DEFAULT 0 NOT NULL;`); - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysAction" integer DEFAULT 0 NOT NULL;`); - await db.execute(sql`ALTER TABLE "resourceSessions" ADD COLUMN "issuedAt" bigint;`); - await db.execute(sql`ALTER TABLE "resources" ADD COLUMN "proxyProtocol" boolean DEFAULT false NOT NULL;`); - await db.execute(sql`ALTER TABLE "resources" ADD COLUMN "proxyProtocolVersion" integer DEFAULT 1;`); - await db.execute(sql`ALTER TABLE "session" ADD COLUMN "issuedAt" bigint;`); - await db.execute(sql`ALTER TABLE "user" ADD COLUMN "lastPasswordChange" bigint;`); - await db.execute(sql`ALTER TABLE "accessAuditLog" ADD CONSTRAINT "accessAuditLog_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`); - await db.execute(sql`ALTER TABLE "actionAuditLog" ADD CONSTRAINT "actionAuditLog_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`); - await db.execute(sql`ALTER TABLE "dnsRecords" ADD CONSTRAINT "dnsRecords_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;`); - await db.execute(sql`ALTER TABLE "requestAuditLog" ADD CONSTRAINT "requestAuditLog_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`); - await db.execute(sql`CREATE INDEX "idx_identityAuditLog_timestamp" ON "accessAuditLog" USING btree ("timestamp");`); - await db.execute(sql`CREATE INDEX "idx_identityAuditLog_org_timestamp" ON "accessAuditLog" USING btree ("orgId","timestamp");`); - await db.execute(sql`CREATE INDEX "idx_actionAuditLog_timestamp" ON "actionAuditLog" USING btree ("timestamp");`); - await db.execute(sql`CREATE INDEX "idx_actionAuditLog_org_timestamp" ON "actionAuditLog" USING btree ("orgId","timestamp");`); - await db.execute(sql`CREATE INDEX "idx_requestAuditLog_timestamp" ON "requestAuditLog" USING btree ("timestamp");`); - await db.execute(sql`CREATE INDEX "idx_requestAuditLog_org_timestamp" ON "requestAuditLog" USING btree ("orgId","timestamp");`); - await db.execute(sql`ALTER TABLE "resources" ADD CONSTRAINT "resources_skipToIdpId_idp_idpId_fk" FOREIGN KEY ("skipToIdpId") REFERENCES "public"."idp"("idpId") ON DELETE set null ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "remoteExitNode" ADD COLUMN "secondaryVersion" varchar;` + ); + await db.execute( + sql`ALTER TABLE "resources" DROP CONSTRAINT "resources_skipToIdpId_idp_idpId_fk";` + ); + await db.execute( + sql`ALTER TABLE "domains" ADD COLUMN "certResolver" varchar;` + ); + await db.execute( + sql`ALTER TABLE "domains" ADD COLUMN "customCertResolver" varchar;` + ); + await db.execute( + sql`ALTER TABLE "domains" ADD COLUMN "preferWildcardCert" boolean;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "requireTwoFactor" boolean;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "maxSessionLengthHours" integer;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "passwordExpiryDays" integer;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysRequest" integer DEFAULT 7 NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysAccess" integer DEFAULT 0 NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysAction" integer DEFAULT 0 NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "resourceSessions" ADD COLUMN "issuedAt" bigint;` + ); + await db.execute( + sql`ALTER TABLE "resources" ADD COLUMN "proxyProtocol" boolean DEFAULT false NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "resources" ADD COLUMN "proxyProtocolVersion" integer DEFAULT 1;` + ); + await db.execute( + sql`ALTER TABLE "session" ADD COLUMN "issuedAt" bigint;` + ); + await db.execute( + sql`ALTER TABLE "user" ADD COLUMN "lastPasswordChange" bigint;` + ); + await db.execute( + sql`ALTER TABLE "accessAuditLog" ADD CONSTRAINT "accessAuditLog_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "actionAuditLog" ADD CONSTRAINT "actionAuditLog_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "dnsRecords" ADD CONSTRAINT "dnsRecords_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "requestAuditLog" ADD CONSTRAINT "requestAuditLog_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`CREATE INDEX "idx_identityAuditLog_timestamp" ON "accessAuditLog" USING btree ("timestamp");` + ); + await db.execute( + sql`CREATE INDEX "idx_identityAuditLog_org_timestamp" ON "accessAuditLog" USING btree ("orgId","timestamp");` + ); + await db.execute( + sql`CREATE INDEX "idx_actionAuditLog_timestamp" ON "actionAuditLog" USING btree ("timestamp");` + ); + await db.execute( + sql`CREATE INDEX "idx_actionAuditLog_org_timestamp" ON "actionAuditLog" USING btree ("orgId","timestamp");` + ); + await db.execute( + sql`CREATE INDEX "idx_requestAuditLog_timestamp" ON "requestAuditLog" USING btree ("timestamp");` + ); + await db.execute( + sql`CREATE INDEX "idx_requestAuditLog_org_timestamp" ON "requestAuditLog" USING btree ("orgId","timestamp");` + ); + await db.execute( + sql`ALTER TABLE "resources" ADD CONSTRAINT "resources_skipToIdpId_idp_idpId_fk" FOREIGN KEY ("skipToIdpId") REFERENCES "public"."idp"("idpId") ON DELETE set null ON UPDATE no action;` + ); await db.execute(sql`ALTER TABLE "orgs" DROP COLUMN "settings";`); - // get all of the domains - const domainsQuery = await db.execute(sql`SELECT "domainId", "baseDomain" FROM "domains"`); + const domainsQuery = await db.execute( + sql`SELECT "domainId", "baseDomain" FROM "domains"` + ); const domains = domainsQuery.rows as { domainId: string; baseDomain: string; @@ -135,11 +194,11 @@ export default async function migration() { // insert two records into the dnsRecords table for each domain await db.execute(sql` INSERT INTO "dnsRecords" ("domainId", "recordType", "baseDomain", "value", "verified") - VALUES (${domain.domainId}, 'A', ${`*.${domain.baseDomain}`}, ${'Server IP Address'}, true) + VALUES (${domain.domainId}, 'A', ${`*.${domain.baseDomain}`}, ${"Server IP Address"}, true) `); await db.execute(sql` INSERT INTO "dnsRecords" ("domainId", "recordType", "baseDomain", "value", "verified") - VALUES (${domain.domainId}, 'A', ${domain.baseDomain}, ${'Server IP Address'}, true) + VALUES (${domain.domainId}, 'A', ${domain.baseDomain}, ${"Server IP Address"}, true) `); } diff --git a/server/setup/scriptsPg/1.13.0.ts b/server/setup/scriptsPg/1.13.0.ts new file mode 100644 index 00000000..9a56706c --- /dev/null +++ b/server/setup/scriptsPg/1.13.0.ts @@ -0,0 +1,382 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; +import { __DIRNAME } from "@server/lib/consts"; +import { readFileSync } from "fs"; +import { join } from "path"; + +const version = "1.13.0"; + +const dev = process.env.ENVIRONMENT !== "prod"; +let file; +if (!dev) { + file = join(__DIRNAME, "names.json"); +} else { + file = join("server/db/names.json"); +} +export const names = JSON.parse(readFileSync(file, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // clean out any non-alphanumeric characters except for dashes + return name.replace(/[^a-z0-9-]/g, ""); +} + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql`BEGIN`); + + await db.execute(sql` + CREATE TABLE "clientSiteResources" ( + "clientId" integer NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "clientSiteResourcesAssociationsCache" ( + "clientId" integer NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "deviceWebAuthCodes" ( + "codeId" serial PRIMARY KEY NOT NULL, + "code" text NOT NULL, + "ip" text, + "city" text, + "deviceName" text, + "applicationName" text NOT NULL, + "expiresAt" bigint NOT NULL, + "createdAt" bigint NOT NULL, + "verified" boolean DEFAULT false NOT NULL, + "userId" varchar, + CONSTRAINT "deviceWebAuthCodes_code_unique" UNIQUE("code") + ); + `); + + await db.execute(sql` + CREATE TABLE "roleSiteResources" ( + "roleId" integer NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "userSiteResources" ( + "userId" varchar NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute( + sql`ALTER TABLE "clientSites" RENAME TO "clientSitesAssociationsCache";` + ); + + await db.execute( + sql`ALTER TABLE "clients" RENAME COLUMN "id" TO "clientId";` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" RENAME COLUMN "destinationIp" TO "destination";` + ); + + await db.execute( + sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_clientId_clients_id_fk";` + ); + + await db.execute( + sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_siteId_sites_siteId_fk";` + ); + + await db.execute( + sql`ALTER TABLE "olms" DROP CONSTRAINT "olms_clientId_clients_id_fk";` + ); + + await db.execute( + sql`ALTER TABLE "roleClients" DROP CONSTRAINT "roleClients_clientId_clients_id_fk";` + ); + + await db.execute( + sql`ALTER TABLE "userClients" DROP CONSTRAINT "userClients_clientId_clients_id_fk";` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ALTER COLUMN "protocol" DROP NOT NULL;` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ALTER COLUMN "proxyPort" DROP NOT NULL;` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ALTER COLUMN "destinationPort" DROP NOT NULL;` + ); + + await db.execute( + sql`ALTER TABLE "clientSitesAssociationsCache" ADD COLUMN "publicKey" varchar;` + ); + + await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "userId" text;`); + + await db.execute( + sql`ALTER TABLE "clients" ADD COLUMN "niceId" varchar NOT NULL DEFAULT 'PLACEHOLDER';` + ); + + await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "olmId" text;`); + + await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "agent" text;`); + + await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "name" varchar;`); + + await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "userId" text;`); + + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "utilitySubnet" varchar;` + ); + + await db.execute( + sql`ALTER TABLE "session" ADD COLUMN "deviceAuthUsed" boolean DEFAULT false NOT NULL;` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "mode" varchar NOT NULL DEFAULT 'host';` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "alias" varchar;` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "aliasAddress" varchar;` + ); + + await db.execute( + sql`ALTER TABLE "targetHealthCheck" ADD COLUMN "hcTlsServerName" text;` + ); + + await db.execute( + sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "deviceWebAuthCodes" ADD CONSTRAINT "deviceWebAuthCodes_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "clients" ADD CONSTRAINT "clients_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE set null ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "roleClients" ADD CONSTRAINT "roleClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;` + ); + + await db.execute( + sql`ALTER TABLE "userClients" ADD CONSTRAINT "userClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;` + ); + + // set 100.96.128.0/24 as the utility subnet on all of the orgs + await db.execute( + sql`UPDATE "orgs" SET "utilitySubnet" = '100.96.128.0/24'` + ); + + // Query all of the sites to get their remoteSubnets + + const sitesRemoteSubnetsData = + await db.execute(sql`SELECT "siteId", "remoteSubnets" FROM "sites" WHERE "remoteSubnets" IS NOT NULL + `); + const sitesRemoteSubnets = sitesRemoteSubnetsData.rows as { + siteId: number; + remoteSubnets: string | null; + }[]; + + await db.execute(sql`ALTER TABLE "sites" DROP COLUMN "remoteSubnets";`); + + // get all of the siteResources and set the the aliasAddress to 100.96.128.x starting at .8 + const siteResourcesData = await db.execute( + sql`SELECT "siteResourceId" FROM "siteResources" ORDER BY "siteResourceId" ASC` + ); + const siteResources = siteResourcesData.rows as { + siteResourceId: number; + }[]; + + let aliasIpOctet = 8; + for (const siteResource of siteResources) { + const aliasAddress = `100.96.128.${aliasIpOctet}`; + await db.execute(sql` + UPDATE "siteResources" SET "aliasAddress" = ${aliasAddress} WHERE "siteResourceId" = ${siteResource.siteResourceId} + `); + aliasIpOctet++; + } + + // For each site with remote subnets we need to create a site resource of type cidr for each remote subnet + for (const site of sitesRemoteSubnets) { + if (site.remoteSubnets) { + // Get the orgId for this site + const siteDataQuery = await db.execute(sql` + SELECT "orgId" FROM "sites" WHERE "siteId" = ${site.siteId} + `); + const siteData = siteDataQuery.rows[0] as + | { orgId: string } + | undefined; + if (!siteData) continue; + + const subnets = site.remoteSubnets.split(","); + for (const subnet of subnets) { + const niceId = generateName(); + await db.execute(sql` + INSERT INTO "siteResources" ("siteId", "orgId", "niceId", "destination", "mode", "name") + VALUES (${site.siteId}, ${siteData.orgId}, ${niceId}, ${subnet.trim()}, 'cidr', 'Remote Subnet'); + `); + } + } + } + + // Associate clients with site resources based on their previous site access + // Get all client-site associations from the renamed clientSitesAssociationsCache table + const clientSiteAssociationsQuery = await db.execute(sql` + SELECT "clientId", "siteId" FROM "clientSitesAssociationsCache" + `); + const clientSiteAssociations = clientSiteAssociationsQuery.rows as { + clientId: number; + siteId: number; + }[]; + + // For each client-site association, find all site resources for that site + for (const association of clientSiteAssociations) { + const siteResourcesQuery = await db.execute(sql` + SELECT "siteResourceId" FROM "siteResources" + WHERE "siteId" = ${association.siteId} + `); + const siteResources = siteResourcesQuery.rows as { + siteResourceId: number; + }[]; + + // Associate the client with all site resources from this site + for (const siteResource of siteResources) { + await db.execute(sql` + INSERT INTO "clientSiteResources" ("clientId", "siteResourceId") + VALUES (${association.clientId}, ${siteResource.siteResourceId}) + `); + // also associate in the clientSiteResourcesAssociationsCache table + await db.execute(sql` + INSERT INTO "clientSiteResourcesAssociationsCache" ("clientId", "siteResourceId") + VALUES (${association.clientId}, ${siteResource.siteResourceId}) + `); + } + } + + // Associate existing site resources with their org's admin role + const siteResourcesWithOrgQuery = await db.execute(sql` + SELECT "siteResourceId", "orgId" FROM "siteResources" + `); + const siteResourcesWithOrg = siteResourcesWithOrgQuery.rows as { + siteResourceId: number; + orgId: string; + }[]; + + for (const siteResource of siteResourcesWithOrg) { + const adminRoleQuery = await db.execute(sql` + SELECT "roleId" FROM "roles" WHERE "orgId" = ${siteResource.orgId} AND "isAdmin" = true LIMIT 1 + `); + const adminRole = adminRoleQuery.rows[0] as + | { roleId: number } + | undefined; + + if (adminRole) { + const existingQuery = await db.execute(sql` + SELECT 1 FROM "roleSiteResources" + WHERE "roleId" = ${adminRole.roleId} AND "siteResourceId" = ${siteResource.siteResourceId} + LIMIT 1 + `); + + if (existingQuery.rows.length === 0) { + await db.execute(sql` + INSERT INTO "roleSiteResources" ("roleId", "siteResourceId") + VALUES (${adminRole.roleId}, ${siteResource.siteResourceId}) + `); + } + } + } + + // Populate niceId for clients + const clientsQuery = await db.execute( + sql`SELECT "clientId" FROM "clients"` + ); + const clients = clientsQuery.rows as { + clientId: number; + }[]; + + const usedNiceIds: string[] = []; + + for (const client of clients) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + await db.execute(sql` + UPDATE "clients" SET "niceId" = ${niceId} WHERE "clientId" = ${client.clientId} + `); + } + + await db.execute(sql`COMMIT`); + console.log("Migrated database"); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Unable to migrate database"); + console.log(e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsPg/1.7.0.ts b/server/setup/scriptsPg/1.7.0.ts index 3cb799e0..aa740ecb 100644 --- a/server/setup/scriptsPg/1.7.0.ts +++ b/server/setup/scriptsPg/1.7.0.ts @@ -121,7 +121,7 @@ export default async function migration() { try { await db.execute(sql`BEGIN`); - + // Update all existing orgs to have the default subnet await db.execute(sql`UPDATE "orgs" SET "subnet" = '100.90.128.0/24'`); diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts index fdbf3ae9..eac7ade9 100644 --- a/server/setup/scriptsPg/1.9.0.ts +++ b/server/setup/scriptsPg/1.9.0.ts @@ -11,7 +11,9 @@ export default async function migration() { try { // Get the first siteId to use as default - const firstSite = await db.execute(sql`SELECT "siteId" FROM "sites" LIMIT 1`); + const firstSite = await db.execute( + sql`SELECT "siteId" FROM "sites" LIMIT 1` + ); if (firstSite.rows.length > 0) { firstSiteId = firstSite.rows[0].siteId as number; } @@ -52,33 +54,59 @@ export default async function migration() { "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 "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 "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 "clientSites" ADD COLUMN "endpoint" varchar;` + ); - await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "online" boolean DEFAULT false NOT NULL;`); + await db.execute( + sql`ALTER TABLE "exitNodes" ADD COLUMN "online" boolean DEFAULT false NOT NULL;` + ); - await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "lastPing" integer;`); + await db.execute( + sql`ALTER TABLE "exitNodes" ADD COLUMN "lastPing" integer;` + ); - await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "type" text DEFAULT 'gerbil';`); + await db.execute( + sql`ALTER TABLE "exitNodes" ADD COLUMN "type" text DEFAULT 'gerbil';` + ); 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`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.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_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 "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 "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 "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";`); diff --git a/server/setup/scriptsSqlite/1.0.0-beta13.ts b/server/setup/scriptsSqlite/1.0.0-beta13.ts index 9ced727f..9986b06f 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta13.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta13.ts @@ -25,7 +25,9 @@ export default async function migration() { console.log(`Added new table and column: resourceRules, applyRules`); } catch (e) { - console.log("Unable to add new table and column: resourceRules, applyRules"); + console.log( + "Unable to add new table and column: resourceRules, applyRules" + ); throw e; } diff --git a/server/setup/scriptsSqlite/1.0.0-beta3.ts b/server/setup/scriptsSqlite/1.0.0-beta3.ts index fccfeb88..5d69af6b 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta3.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta3.ts @@ -38,4 +38,4 @@ export default async function migration() { fs.writeFileSync(filePath, updatedYaml, "utf8"); console.log("Done."); -} \ No newline at end of file +} diff --git a/server/setup/scriptsSqlite/1.0.0-beta6.ts b/server/setup/scriptsSqlite/1.0.0-beta6.ts index 89129678..a13a7e31 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta6.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta6.ts @@ -43,7 +43,9 @@ export default async function migration() { const updatedYaml = yaml.dump(rawConfig); fs.writeFileSync(filePath, updatedYaml, "utf8"); } catch (error) { - console.log("We were unable to add CORS to your config file. Please add it manually."); + console.log( + "We were unable to add CORS to your config file. Please add it manually." + ); console.error(error); } diff --git a/server/setup/scriptsSqlite/1.0.0-beta9.ts b/server/setup/scriptsSqlite/1.0.0-beta9.ts index 7cce1c2d..6d48ed39 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta9.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta9.ts @@ -182,12 +182,15 @@ export default async function migration() { if (parsedConfig.success) { // delete permanent from redirect-to-https middleware - delete traefikConfig.http.middlewares["redirect-to-https"].redirectScheme.permanent; + delete traefikConfig.http.middlewares["redirect-to-https"] + .redirectScheme.permanent; const updatedTraefikYaml = yaml.dump(traefikConfig); fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8"); - console.log("Deleted permanent from redirect-to-https middleware."); + console.log( + "Deleted permanent from redirect-to-https middleware." + ); } else { console.log(fromZodError(parsedConfig.error)); console.log( diff --git a/server/setup/scriptsSqlite/1.10.0.ts b/server/setup/scriptsSqlite/1.10.0.ts index 3065a664..03cf24dc 100644 --- a/server/setup/scriptsSqlite/1.10.0.ts +++ b/server/setup/scriptsSqlite/1.10.0.ts @@ -13,15 +13,11 @@ export default async function migration() { try { const resources = db - .prepare( - "SELECT resourceId FROM resources" - ) + .prepare("SELECT resourceId FROM resources") .all() as Array<{ resourceId: number }>; const siteResources = db - .prepare( - "SELECT siteResourceId FROM siteResources" - ) + .prepare("SELECT siteResourceId FROM siteResources") .all() as Array<{ siteResourceId: number }>; db.transaction(() => { @@ -82,17 +78,13 @@ export default async function migration() { // Handle auto-provisioned users for identity providers const autoProvisionIdps = db - .prepare( - "SELECT idpId FROM idp WHERE autoProvision = 1" - ) + .prepare("SELECT idpId FROM idp WHERE autoProvision = 1") .all() as Array<{ idpId: number }>; for (const idp of autoProvisionIdps) { // Get all users with this identity provider const usersWithIdp = db - .prepare( - "SELECT id FROM user WHERE idpId = ?" - ) + .prepare("SELECT id FROM user WHERE idpId = ?") .all(idp.idpId) as Array<{ id: string }>; // Update userOrgs to set autoProvisioned to true for these users diff --git a/server/setup/scriptsSqlite/1.10.1.ts b/server/setup/scriptsSqlite/1.10.1.ts index f6f9894e..24181558 100644 --- a/server/setup/scriptsSqlite/1.10.1.ts +++ b/server/setup/scriptsSqlite/1.10.1.ts @@ -5,16 +5,16 @@ import path from "path"; const version = "1.10.1"; export default async function migration() { - console.log(`Running setup script ${version}...`); + console.log(`Running setup script ${version}...`); - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); - try { - db.pragma("foreign_keys = OFF"); + try { + db.pragma("foreign_keys = OFF"); - db.transaction(() => { - db.exec(`ALTER TABLE "targets" RENAME TO "targets_old"; + db.transaction(() => { + db.exec(`ALTER TABLE "targets" RENAME TO "targets_old"; --> statement-breakpoint CREATE TABLE "targets" ( "targetId" INTEGER PRIMARY KEY AUTOINCREMENT, @@ -57,13 +57,13 @@ SELECT FROM "targets_old"; --> statement-breakpoint DROP TABLE "targets_old";`); - })(); + })(); - db.pragma("foreign_keys = ON"); + db.pragma("foreign_keys = ON"); - console.log(`Migrated database`); - } catch (e) { - console.log("Failed to migrate db:", e); - throw e; - } -} \ No newline at end of file + console.log(`Migrated database`); + } catch (e) { + console.log("Failed to migrate db:", e); + throw e; + } +} diff --git a/server/setup/scriptsSqlite/1.11.0.ts b/server/setup/scriptsSqlite/1.11.0.ts index c79cfdb4..41d68563 100644 --- a/server/setup/scriptsSqlite/1.11.0.ts +++ b/server/setup/scriptsSqlite/1.11.0.ts @@ -13,25 +13,29 @@ export default async function migration() { const db = new Database(location); db.transaction(() => { - - db.prepare(` + db.prepare( + ` CREATE TABLE 'account' ( 'accountId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'userId' text NOT NULL, FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'accountDomains' ( 'accountId' integer NOT NULL, 'domainId' text NOT NULL, FOREIGN KEY ('accountId') REFERENCES 'account'('accountId') ON UPDATE no action ON DELETE cascade, FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'certificates' ( 'certId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'domain' text NOT NULL, @@ -49,11 +53,15 @@ export default async function migration() { 'keyFile' text, FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(`CREATE UNIQUE INDEX 'certificates_domain_unique' ON 'certificates' ('domain');`).run(); + db.prepare( + `CREATE UNIQUE INDEX 'certificates_domain_unique' ON 'certificates' ('domain');` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'customers' ( 'customerId' text PRIMARY KEY NOT NULL, 'orgId' text NOT NULL, @@ -65,9 +73,11 @@ export default async function migration() { 'updatedAt' integer NOT NULL, FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'dnsChallenges' ( 'dnsChallengeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'domain' text NOT NULL, @@ -77,26 +87,32 @@ export default async function migration() { 'expiresAt' integer NOT NULL, 'completed' integer DEFAULT false ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'domainNamespaces' ( 'domainNamespaceId' text PRIMARY KEY NOT NULL, 'domainId' text NOT NULL, FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'exitNodeOrgs' ( 'exitNodeId' integer NOT NULL, 'orgId' text NOT NULL, FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE cascade, FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'loginPage' ( 'loginPageId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'subdomain' text, @@ -106,27 +122,33 @@ export default async function migration() { FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null, FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'loginPageOrg' ( 'loginPageId' integer NOT NULL, 'orgId' text NOT NULL, FOREIGN KEY ('loginPageId') REFERENCES 'loginPage'('loginPageId') ON UPDATE no action ON DELETE cascade, FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'remoteExitNodeSession' ( 'id' text PRIMARY KEY NOT NULL, 'remoteExitNodeId' text NOT NULL, 'expiresAt' integer NOT NULL, FOREIGN KEY ('remoteExitNodeId') REFERENCES 'remoteExitNode'('id') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'remoteExitNode' ( 'id' text PRIMARY KEY NOT NULL, 'secretHash' text NOT NULL, @@ -135,9 +157,11 @@ export default async function migration() { 'exitNodeId' integer, FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'sessionTransferToken' ( 'token' text PRIMARY KEY NOT NULL, 'sessionId' text NOT NULL, @@ -145,9 +169,11 @@ export default async function migration() { 'expiresAt' integer NOT NULL, FOREIGN KEY ('sessionId') REFERENCES 'session'('id') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'subscriptionItems' ( 'subscriptionItemId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'subscriptionId' text NOT NULL, @@ -162,9 +188,11 @@ export default async function migration() { 'name' text, FOREIGN KEY ('subscriptionId') REFERENCES 'subscriptions'('subscriptionId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'subscriptions' ( 'subscriptionId' text PRIMARY KEY NOT NULL, 'customerId' text NOT NULL, @@ -175,9 +203,11 @@ export default async function migration() { 'billingCycleAnchor' integer, FOREIGN KEY ('customerId') REFERENCES 'customers'('customerId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'usage' ( 'usageId' text PRIMARY KEY NOT NULL, 'featureId' text NOT NULL, @@ -191,9 +221,11 @@ export default async function migration() { 'nextRolloverAt' integer, FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'usageNotifications' ( 'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'orgId' text NOT NULL, @@ -203,18 +235,22 @@ export default async function migration() { 'sentAt' integer NOT NULL, FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'resourceHeaderAuth' ( 'headerAuthId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'resourceId' integer NOT NULL, 'headerAuthHash' text NOT NULL, FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'targetHealthCheck' ( 'targetHealthCheckId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, 'targetId' integer NOT NULL, @@ -234,11 +270,13 @@ export default async function migration() { 'hcHealth' text DEFAULT 'unknown', FOREIGN KEY ('targetId') REFERENCES 'targets'('targetId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); db.prepare(`DROP TABLE 'limits';`).run(); - db.prepare(` + db.prepare( + ` CREATE TABLE 'limits' ( 'limitId' text PRIMARY KEY NOT NULL, 'featureId' text NOT NULL, @@ -247,12 +285,15 @@ export default async function migration() { 'description' text, FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade ); - `).run(); + ` + ).run(); db.prepare(`ALTER TABLE 'orgs' ADD 'settings' text;`).run(); db.prepare(`ALTER TABLE 'targets' ADD 'rewritePath' text;`).run(); db.prepare(`ALTER TABLE 'targets' ADD 'rewritePathType' text;`).run(); - db.prepare(`ALTER TABLE 'targets' ADD 'priority' integer DEFAULT 100 NOT NULL;`).run(); + db.prepare( + `ALTER TABLE 'targets' ADD 'priority' integer DEFAULT 100 NOT NULL;` + ).run(); const webauthnCredentials = db .prepare( @@ -269,7 +310,7 @@ export default async function migration() { dateCreated: string; }[]; - db.prepare(`DELETE FROM 'webauthnCredentials';`).run(); + db.prepare(`DELETE FROM 'webauthnCredentials';`).run(); for (const webauthnCredential of webauthnCredentials) { const newCredentialId = isoBase64URL.fromBuffer( @@ -304,7 +345,9 @@ export default async function migration() { ).run(); // 2. Select all rows - const resources = db.prepare(`SELECT resourceId FROM resources`).all() as { + const resources = db + .prepare(`SELECT resourceId FROM resources`) + .all() as { resourceId: number; }[]; diff --git a/server/setup/scriptsSqlite/1.12.0.ts b/server/setup/scriptsSqlite/1.12.0.ts index bb357c81..292f1f05 100644 --- a/server/setup/scriptsSqlite/1.12.0.ts +++ b/server/setup/scriptsSqlite/1.12.0.ts @@ -112,7 +112,6 @@ export default async function migration() { ` ).run(); - db.prepare( ` CREATE TABLE 'blueprints' ( @@ -212,10 +211,14 @@ export default async function migration() { db.prepare( `ALTER TABLE 'user' ADD 'lastPasswordChange' integer;` ).run(); - db.prepare(`ALTER TABLE 'remoteExitNode' ADD 'secondaryVersion' text;`).run(); + db.prepare( + `ALTER TABLE 'remoteExitNode' ADD 'secondaryVersion' text;` + ).run(); // get all of the domains - const domains = db.prepare(`SELECT domainId, baseDomain from domains`).all() as { + const domains = db + .prepare(`SELECT domainId, baseDomain from domains`) + .all() as { domainId: number; baseDomain: string; }[]; diff --git a/server/setup/scriptsSqlite/1.13.0.ts b/server/setup/scriptsSqlite/1.13.0.ts new file mode 100644 index 00000000..df8d7344 --- /dev/null +++ b/server/setup/scriptsSqlite/1.13.0.ts @@ -0,0 +1,444 @@ +import { __DIRNAME, APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import { readFileSync } from "fs"; +import path, { join } from "path"; + +const version = "1.13.0"; + +const dev = process.env.ENVIRONMENT !== "prod"; +let file; +if (!dev) { + file = join(__DIRNAME, "names.json"); +} else { + file = join("server/db/names.json"); +} +export const names = JSON.parse(readFileSync(file, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // clean out any non-alphanumeric characters except for dashes + return name.replace(/[^a-z0-9-]/g, ""); +} + +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.prepare( + `ALTER TABLE 'clientSites' RENAME TO 'clientSitesAssociationsCache';` + ).run(); + + db.prepare( + `ALTER TABLE 'clients' RENAME COLUMN 'id' TO 'clientId';` + ).run(); + + db.prepare( + ` + CREATE TABLE 'clientSiteResources' ( + 'clientId' integer NOT NULL, + 'siteResourceId' integer NOT NULL, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE 'clientSiteResourcesAssociationsCache' ( + 'clientId' integer NOT NULL, + 'siteResourceId' integer NOT NULL + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE 'deviceWebAuthCodes' ( + 'codeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'code' text NOT NULL, + 'ip' text, + 'city' text, + 'deviceName' text, + 'applicationName' text NOT NULL, + 'expiresAt' integer NOT NULL, + 'createdAt' integer NOT NULL, + 'verified' integer DEFAULT false NOT NULL, + 'userId' text, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `CREATE UNIQUE INDEX 'deviceWebAuthCodes_code_unique' ON 'deviceWebAuthCodes' ('code');` + ).run(); + + db.prepare( + ` + CREATE TABLE 'roleSiteResources' ( + 'roleId' integer NOT NULL, + 'siteResourceId' integer NOT NULL, + FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE 'userSiteResources' ( + 'userId' text NOT NULL, + 'siteResourceId' integer NOT NULL, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE '__new_clientSitesAssociationsCache' ( + 'clientId' integer NOT NULL, + 'siteId' integer NOT NULL, + 'isRelayed' integer DEFAULT false NOT NULL, + 'endpoint' text, + 'publicKey' text + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_clientSitesAssociationsCache'("clientId", "siteId", "isRelayed", "endpoint", "publicKey") SELECT "clientId", "siteId", "isRelayed", "endpoint", NULL FROM 'clientSitesAssociationsCache';` + ).run(); + + db.prepare(`DROP TABLE 'clientSitesAssociationsCache';`).run(); + + db.prepare( + `ALTER TABLE '__new_clientSitesAssociationsCache' RENAME TO 'clientSitesAssociationsCache';` + ).run(); + + db.prepare( + `ALTER TABLE 'clients' ADD 'userId' text REFERENCES 'user'('id');` + ).run(); + + db.prepare( + `ALTER TABLE 'clients' ADD COLUMN 'niceId' TEXT NOT NULL DEFAULT 'PLACEHOLDER';` + ).run(); + + db.prepare(`ALTER TABLE 'clients' ADD 'olmId' text;`).run(); + + db.prepare( + ` + CREATE TABLE '__new_siteResources' ( + 'siteResourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'siteId' integer NOT NULL, + 'orgId' text NOT NULL, + 'niceId' text NOT NULL, + 'name' text NOT NULL, + 'mode' text NOT NULL, + 'protocol' text, + 'proxyPort' integer, + 'destinationPort' integer, + 'destination' text NOT NULL, + 'enabled' integer DEFAULT true NOT NULL, + 'alias' text, + 'aliasAddress' text, + 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 + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_siteResources'("siteResourceId", "siteId", "orgId", "niceId", "name", "mode", "protocol", "proxyPort", "destinationPort", "destination", "enabled", "alias", "aliasAddress") SELECT "siteResourceId", "siteId", "orgId", "niceId", "name", 'host', "protocol", "proxyPort", "destinationPort", "destinationIp", "enabled", NULL, NULL FROM 'siteResources';` + ).run(); + + db.prepare(`DROP TABLE 'siteResources';`).run(); + + db.prepare( + `ALTER TABLE '__new_siteResources' RENAME TO 'siteResources';` + ).run(); + + db.prepare( + ` + CREATE TABLE '__new_olms' ( + 'id' text PRIMARY KEY NOT NULL, + 'secretHash' text NOT NULL, + 'dateCreated' text NOT NULL, + 'version' text, + 'agent' text, + 'name' text, + 'clientId' integer, + 'userId' text, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE set null, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_olms'("id", "secretHash", "dateCreated", "version", "agent", "name", "clientId", "userId") SELECT "id", "secretHash", "dateCreated", "version", NULL, NULL, "clientId", NULL FROM 'olms';` + ).run(); + + db.prepare(`DROP TABLE 'olms';`).run(); + + db.prepare(`ALTER TABLE '__new_olms' RENAME TO 'olms';`).run(); + + db.prepare( + ` + CREATE TABLE '__new_roleClients' ( + 'roleId' integer NOT NULL, + 'clientId' integer NOT NULL, + FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_roleClients'("roleId", "clientId") SELECT "roleId", "clientId" FROM 'roleClients';` + ).run(); + + db.prepare(`DROP TABLE 'roleClients';`).run(); + + db.prepare( + `ALTER TABLE '__new_roleClients' RENAME TO 'roleClients';` + ).run(); + + db.prepare( + ` + CREATE TABLE '__new_userClients' ( + 'userId' text NOT NULL, + 'clientId' integer NOT NULL, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_userClients'("userId", "clientId") SELECT "userId", "clientId" FROM 'userClients';` + ).run(); + + db.prepare(`DROP TABLE 'userClients';`).run(); + + db.prepare( + `ALTER TABLE '__new_userClients' RENAME TO 'userClients';` + ).run(); + + db.prepare(`ALTER TABLE 'orgs' ADD 'utilitySubnet' text;`).run(); + + db.prepare( + `ALTER TABLE 'session' ADD 'deviceAuthUsed' integer DEFAULT false NOT NULL;` + ).run(); + + db.prepare( + `ALTER TABLE 'targetHealthCheck' ADD 'hcTlsServerName' text;` + ).run(); + + // set 100.96.128.0/24 as the utility subnet on all of the orgs + db.prepare( + `UPDATE 'orgs' SET 'utilitySubnet' = '100.96.128.0/24'` + ).run(); + + // Query all of the sites to get their remoteSubnets before dropping the column + const sitesRemoteSubnets = db + .prepare( + `SELECT siteId, remoteSubnets FROM 'sites' WHERE remoteSubnets IS NOT NULL` + ) + .all() as { + siteId: number; + remoteSubnets: string | null; + }[]; + + db.prepare( + `ALTER TABLE 'sites' DROP COLUMN 'remoteSubnets';` + ).run(); + + // get all of the siteResources and set the aliasAddress to 100.96.128.x starting at .8 + const siteResourcesForAlias = db + .prepare( + `SELECT siteResourceId FROM 'siteResources' ORDER BY siteResourceId ASC` + ) + .all() as { + siteResourceId: number; + }[]; + + const updateAliasAddress = db.prepare( + `UPDATE 'siteResources' SET aliasAddress = ? WHERE siteResourceId = ?` + ); + + let aliasIpOctet = 8; + for (const siteResource of siteResourcesForAlias) { + const aliasAddress = `100.96.128.${aliasIpOctet}`; + updateAliasAddress.run( + aliasAddress, + siteResource.siteResourceId + ); + aliasIpOctet++; + } + + // For each site with remote subnets we need to create a site resource of type cidr for each remote subnet + const insertCidrResource = db.prepare( + `INSERT INTO 'siteResources' ('siteId', 'destination', 'mode', 'name', 'orgId', 'niceId') + SELECT ?, ?, 'cidr', 'Remote Subnet', orgId, ? FROM 'sites' WHERE siteId = ?` + ); + + for (const site of sitesRemoteSubnets) { + if (site.remoteSubnets) { + const subnets = site.remoteSubnets.split(","); + for (const subnet of subnets) { + // Generate a unique niceId for each new site resource + let niceId = generateName(); + insertCidrResource.run( + site.siteId, + subnet.trim(), + niceId, + site.siteId + ); + } + } + } + + // Associate clients with site resources based on their previous site access + // Get all client-site associations from the renamed clientSitesAssociationsCache table + const clientSiteAssociations = db + .prepare( + `SELECT clientId, siteId FROM 'clientSitesAssociationsCache'` + ) + .all() as { + clientId: number; + siteId: number; + }[]; + + const getSiteResources = db.prepare( + `SELECT siteResourceId FROM 'siteResources' WHERE siteId = ?` + ); + + const insertClientSiteResource = db.prepare( + `INSERT INTO 'clientSiteResources' ('clientId', 'siteResourceId') VALUES (?, ?)` + ); + + // create a clientSiteResourcesAssociationsCache entry for each existing association as well + const insertClientSiteResourceCache = db.prepare( + `INSERT INTO 'clientSiteResourcesAssociationsCache' ('clientId', 'siteResourceId') VALUES (?, ?)` + ); + + // For each client-site association, find all site resources for that site + for (const association of clientSiteAssociations) { + const siteResources = getSiteResources.all( + association.siteId + ) as { + siteResourceId: number; + }[]; + + // Associate the client with all site resources from this site + for (const siteResource of siteResources) { + insertClientSiteResource.run( + association.clientId, + siteResource.siteResourceId + ); + insertClientSiteResourceCache.run( + association.clientId, + siteResource.siteResourceId + ); + } + } + + // Associate existing site resources with their org's admin role + const siteResourcesWithOrg = db + .prepare(`SELECT siteResourceId, orgId FROM 'siteResources'`) + .all() as { + siteResourceId: number; + orgId: string; + }[]; + + const getAdminRole = db.prepare( + `SELECT roleId FROM 'roles' WHERE orgId = ? AND isAdmin = 1 LIMIT 1` + ); + + const checkExistingAssociation = db.prepare( + `SELECT 1 FROM 'roleSiteResources' WHERE roleId = ? AND siteResourceId = ? LIMIT 1` + ); + + const insertRoleSiteResource = db.prepare( + `INSERT INTO 'roleSiteResources' ('roleId', 'siteResourceId') VALUES (?, ?)` + ); + + for (const siteResource of siteResourcesWithOrg) { + const adminRole = getAdminRole.get(siteResource.orgId) as + | { roleId: number } + | undefined; + + if (adminRole) { + const existing = checkExistingAssociation.get( + adminRole.roleId, + siteResource.siteResourceId + ); + + if (!existing) { + insertRoleSiteResource.run( + adminRole.roleId, + siteResource.siteResourceId + ); + } + } + } + + // Populate niceId for clients + const clients = db + .prepare(`SELECT clientId FROM 'clients'`) + .all() as { + clientId: number; + }[]; + + const usedNiceIds: string[] = []; + + for (const clientId of clients) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + db.prepare( + `UPDATE clients SET niceId = ? WHERE clientId = ?` + ).run(niceId, clientId.clientId); + } + })(); + + db.pragma("foreign_keys = ON"); + + console.log(`Migrated database`); + } catch (e) { + console.log("Failed to migrate db:", e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsSqlite/1.5.0.ts b/server/setup/scriptsSqlite/1.5.0.ts index 46e9ccca..10c12294 100644 --- a/server/setup/scriptsSqlite/1.5.0.ts +++ b/server/setup/scriptsSqlite/1.5.0.ts @@ -48,9 +48,7 @@ export default async function migration() { const rawConfig = yaml.load(fileContents) as any; if (rawConfig.cors?.headers) { - const headers = JSON.parse( - JSON.stringify(rawConfig.cors.headers) - ); + const headers = JSON.parse(JSON.stringify(rawConfig.cors.headers)); rawConfig.cors.allowed_headers = headers; delete rawConfig.cors.headers; } @@ -61,9 +59,7 @@ export default async function migration() { console.log(`Migrated CORS headers to allowed_headers`); } catch (e) { - console.log( - `Unable to migrate config file. Error: ${e}` - ); + console.log(`Unable to migrate config file. Error: ${e}`); } console.log(`${version} migration complete`); diff --git a/server/setup/scriptsSqlite/1.6.0.ts b/server/setup/scriptsSqlite/1.6.0.ts index adab2697..45abe693 100644 --- a/server/setup/scriptsSqlite/1.6.0.ts +++ b/server/setup/scriptsSqlite/1.6.0.ts @@ -58,7 +58,9 @@ export default async function migration() { console.log(`Set trust_proxy to 1 in config file`); } catch (e) { - console.log(`Unable to migrate config file. Please do it manually. Error: ${e}`); + console.log( + `Unable to migrate config file. Please do it manually. Error: ${e}` + ); } console.log(`${version} migration complete`); diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts index 5f247ea5..89d7b595 100644 --- a/server/setup/scriptsSqlite/1.9.0.ts +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -11,26 +11,28 @@ export default async function migration() { const db = new Database(location); const resourceSiteMap = new Map(); - let firstSiteId: number = 1; + let firstSiteId: number = 1; - 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; - } + 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); - } + 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"); diff --git a/server/types/HttpCode.ts b/server/types/HttpCode.ts index 70f21053..a20c8577 100644 --- a/server/types/HttpCode.ts +++ b/server/types/HttpCode.ts @@ -59,7 +59,7 @@ export enum HttpCode { INSUFFICIENT_STORAGE = 507, LOOP_DETECTED = 508, NOT_EXTENDED = 510, - NETWORK_AUTHENTICATION_REQUIRED = 511, + NETWORK_AUTHENTICATION_REQUIRED = 511 } export default HttpCode; diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index 538c7fde..e52f19ed 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -10,7 +10,7 @@ import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; type BillingSettingsProps = { children: React.ReactNode; @@ -19,7 +19,7 @@ type BillingSettingsProps = { export default async function BillingSettingsPage({ children, - params, + params }: BillingSettingsProps) { const { orgId } = await params; @@ -35,8 +35,8 @@ export default async function BillingSettingsPage({ const getOrgUser = cache(async () => internal.get>( `/org/${orgId}/user/${user.userId}`, - await authCookieHeader(), - ), + await authCookieHeader() + ) ); const res = await getOrgUser(); orgUser = res.data.data; @@ -49,8 +49,8 @@ export default async function BillingSettingsPage({ const getOrg = cache(async () => internal.get>( `/org/${orgId}`, - await authCookieHeader(), - ), + await authCookieHeader() + ) ); const res = await getOrg(); org = res.data.data; @@ -65,11 +65,11 @@ export default async function BillingSettingsPage({ - {children} + {children} diff --git a/src/app/[orgId]/settings/(private)/idp/create/page.tsx b/src/app/[orgId]/settings/(private)/idp/create/page.tsx index 8667abda..a899a2aa 100644 --- a/src/app/[orgId]/settings/(private)/idp/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/create/page.tsx @@ -64,10 +64,8 @@ export default function Page() { clientSecret: z .string() .min(1, { message: t("idpClientSecretRequired") }), - authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }) - .optional(), - tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }) - .optional(), + authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }).optional(), + tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }).optional(), identifierPath: z .string() .min(1, { message: t("idpPathRequired") }) @@ -379,9 +377,11 @@ export default function Page() { > { form.setValue( "autoProvision", diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx index a1bb69c0..c12aa9ba 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx @@ -19,18 +19,17 @@ export function ExitNodesDataTable({ onRefresh, isRefreshing }: DataTableProps) { - const t = useTranslations(); return ( { const originalRow = row.original; - return originalRow.version || "-"; + return ( +
+ {originalRow.version && originalRow.version ? ( + + {"v" + originalRow.version} + + ) : ( + "-" + )} +
+ ); } }, { id: "actions", - header: () => ({t("actions")}), + header: () => {t("actions")}, cell: ({ row }) => { const nodeRow = row.original; const remoteExitNodeId = nodeRow.id; @@ -295,9 +305,7 @@ export default function ExitNodesTable({ }} dialog={
-

- {t("remoteExitNodeQuestionRemove")} -

+

{t("remoteExitNodeQuestionRemove")}

{t("remoteExitNodeMessageRemove")}

diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx index 115b1bd3..4f66b7f8 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx @@ -6,6 +6,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -21,11 +22,20 @@ import { QuickStartRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext"; -import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { build } from "@server/build"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon } from "lucide-react"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -36,7 +46,16 @@ export default function CredentialsPage() { const { remoteExitNode } = useRemoteExitNodeContext(); const [modalOpen, setModalOpen] = useState(false); - const [credentials, setCredentials] = useState(null); + const [credentials, setCredentials] = + useState(null); + const [currentRemoteExitNodeId, setCurrentRemoteExitNodeId] = useState< + string | null + >(remoteExitNode.remoteExitNodeId); + const [regeneratedSecret, setRegeneratedSecret] = useState( + null + ); + const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); + const [shouldDisconnect, setShouldDisconnect] = useState(true); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); @@ -48,86 +67,213 @@ export default function CredentialsPage() { return isEnterpriseNotLicensed || isSaasNotSubscribed; }; - const handleConfirmRegenerate = async () => { + try { + const response = await api.get< + AxiosResponse + >(`/org/${orgId}/pick-remote-exit-node-defaults`); - const response = await api.get>( - `/org/${orgId}/pick-remote-exit-node-defaults` - ); + const data = response.data.data; + setCredentials(data); - const data = response.data.data; - setCredentials(data); - - await api.put>( - `/re-key/${orgId}/reGenerate-remote-exit-node-secret`, - { + const rekeyRes = await api.put< + AxiosResponse + >(`/re-key/${orgId}/regenerate-remote-exit-node-secret`, { remoteExitNodeId: remoteExitNode.remoteExitNodeId, secret: data.secret, + disconnect: shouldDisconnect + }); + + if (rekeyRes && rekeyRes.status === 200) { + const rekeyData = rekeyRes.data.data; + if (rekeyData && rekeyData.remoteExitNodeId) { + setCurrentRemoteExitNodeId(rekeyData.remoteExitNodeId); + setRegeneratedSecret(data.secret); + setCredentials({ + ...data, + remoteExitNodeId: rekeyData.remoteExitNodeId + }); + setShowCredentialsAlert(true); + } } - ); - toast({ - title: t("credentialsSaved"), - description: t("credentialsSavedDescription") - }); - - router.refresh(); - }; - - const getCredentials = () => { - if (credentials) { - return { - Id: remoteExitNode.remoteExitNodeId, - Secret: credentials.secret - }; + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + } catch (error) { + toast({ + variant: "destructive", + title: t("error") || "Error", + description: + formatAxiosError(error) || + t("credentialsRegenerateError") || + "Failed to regenerate credentials" + }); } - return undefined; }; + const getConfirmationString = () => { + return ( + remoteExitNode?.name || + remoteExitNode?.remoteExitNodeId || + "My remote exit node" + ); + }; + + const displayRemoteExitNodeId = + currentRemoteExitNodeId || remoteExitNode?.remoteExitNodeId || null; + const displaySecret = regeneratedSecret || null; + return ( - - - - - {t("generatedcredentials")} - - - {t("regenerateCredentials")} - - + <> + + + + + {t("generatedcredentials")} + + + {t("regenerateCredentials")} + + + + - - - - -
- -
-
+ + + + {t("endpoint") || "Endpoint"} + + + + + + + + {t("remoteExitNodeId") || + "Remote Exit Node ID"} + + + {displayRemoteExitNodeId ? ( + + ) : ( + {"••••••••••••••••"} + )} + + + + + {t("secretKey") || "Secret Key"} + + + {displaySecret ? ( + + ) : ( + + {"••••••••••••••••••••••••••••••••"} + + )} + + + - {isSecurityFeatureDisabled() && ( - - {t("featureDisabledTooltip")} - - )} -
-
-
-
+ {showCredentialsAlert && displaySecret && ( + + + + {t("credentialsSave") || + "Save the Credentials"} + + + {t("credentialsSaveDescription") || + "You will only be able to see this once. Make sure to copy it to a secure place."} + + + )} + + {build !== "oss" && ( + + + + + )} +
+
- { + setModalOpen(val); + // Prevent modal from reopening during refresh + if (!val) { + setTimeout(() => { + router.refresh(); + }, 150); + } + }} + dialog={ +
+ {shouldDisconnect ? ( + <> +

+ {t( + "remoteExitNodeRegenerateAndDisconnectConfirmation" + )} +

+

+ {t( + "remoteExitNodeRegenerateAndDisconnectWarning" + )} +

+ + ) : ( + <> +

+ {t( + "remoteExitNodeRegenerateCredentialsConfirmation" + )} +

+

+ {t( + "remoteExitNodeRegenerateCredentialsWarning" + )} +

+ + )} +
+ } + buttonText={ + shouldDisconnect + ? t("remoteExitNodeRegenerateAndDisconnect") + : t("regenerateCredentialsButton") + } + onConfirm={handleConfirmRegenerate} + string={getConfirmationString()} + title={t("regenerateCredentials")} + warningText={t("cannotbeUndone")} /> - + ); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx index 19357a7f..98af49a6 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx @@ -35,7 +35,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const navItems = [ { - title: t('credentials'), + title: t("credentials"), href: "/{orgId}/settings/remote-exit-nodes/{remoteExitNodeId}/credentials" } ]; diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx index ca3e0cba..062b7e9a 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx @@ -86,7 +86,7 @@ export default function CreateRemoteExitNodePage() { useEffect(() => { const remoteExitNodeId = searchParams.get("remoteExitNodeId"); const remoteExitNodeSecret = searchParams.get("remoteExitNodeSecret"); - + if (remoteExitNodeId && remoteExitNodeSecret) { setStrategy("adopt"); form.setValue("remoteExitNodeId", remoteExitNodeId); diff --git a/src/app/[orgId]/settings/access/invitations/page.tsx b/src/app/[orgId]/settings/access/invitations/page.tsx index d7fee322..b6ee1448 100644 --- a/src/app/[orgId]/settings/access/invitations/page.tsx +++ b/src/app/[orgId]/settings/access/invitations/page.tsx @@ -1,7 +1,9 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; -import InvitationsTable, { InvitationRow } from "../../../../../components/InvitationsTable"; +import InvitationsTable, { + InvitationRow +} from "../../../../../components/InvitationsTable"; import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; @@ -9,7 +11,7 @@ import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; type InvitationsPageProps = { params: Promise<{ orgId: string }>; @@ -68,7 +70,7 @@ export default async function InvitationsPage(props: InvitationsPageProps) { id: invite.inviteId, email: invite.email, expiresAt: new Date(Number(invite.expiresAt)).toISOString(), - role: invite.roleName || t('accessRoleUnknown'), + role: invite.roleName || t("accessRoleUnknown"), roleId: invite.roleId }; }); @@ -76,8 +78,8 @@ export default async function InvitationsPage(props: InvitationsPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/access/roles/page.tsx b/src/app/[orgId]/settings/access/roles/page.tsx index cffe4ed9..c4818abe 100644 --- a/src/app/[orgId]/settings/access/roles/page.tsx +++ b/src/app/[orgId]/settings/access/roles/page.tsx @@ -7,7 +7,7 @@ import OrgProvider from "@app/providers/OrgProvider"; import { ListRolesResponse } from "@server/routers/role"; import RolesTable, { RoleRow } from "../../../../../components/RolesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; type RolesPageProps = { params: Promise<{ orgId: string }>; @@ -66,8 +66,8 @@ export default async function RolesPage(props: RolesPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index 9417282d..3199d817 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -106,7 +106,8 @@ export default function Page() { const genericOidcFormSchema = z.object({ username: z.string().min(1, { message: t("usernameRequired") }), - email: z.email({ message: t("emailInvalid") }) + email: z + .email({ message: t("emailInvalid") }) .optional() .or(z.literal("")), name: z.string().optional(), diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 453aca2f..662ada60 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -10,7 +10,7 @@ import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; type UsersPageProps = { params: Promise<{ orgId: string }>; @@ -79,9 +79,11 @@ export default async function UsersPage(props: UsersPageProps) { type: user.type, idpVariant: user.idpVariant, idpId: user.idpId, - idpName: user.idpName || t('idpNameInternal'), - status: t('userConfirmed'), - role: user.isOwner ? t('accessRoleOwner') : user.roleName || t('accessRoleMember'), + idpName: user.idpName || t("idpNameInternal"), + status: t("userConfirmed"), + role: user.isOwner + ? t("accessRoleOwner") + : user.roleName || t("accessRoleMember"), isOwner: user.isOwner || false }; }); @@ -89,8 +91,8 @@ export default async function UsersPage(props: UsersPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx index e012f332..19b695ca 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx @@ -6,7 +6,7 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { GetApiKeyResponse } from "@server/routers/apiKeys"; import ApiKeyProvider from "@app/providers/ApiKeyProvider"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; interface SettingsLayoutProps { children: React.ReactNode; @@ -33,14 +33,16 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const navItems = [ { - title: t('apiKeysPermissionsTitle'), + title: t("apiKeysPermissionsTitle"), href: "/{orgId}/settings/api-keys/{apiKeyId}/permissions" } ]; return ( <> - + {children} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx index e54f442d..518db250 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx @@ -4,5 +4,7 @@ export default async function ApiKeysPage(props: { params: Promise<{ orgId: string; apiKeyId: string }>; }) { const params = await props.params; - redirect(`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`); + redirect( + `/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions` + ); } diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx index 121d9523..da63c9c5 100644 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx @@ -45,10 +45,10 @@ export default function Page() { .catch((e) => { toast({ variant: "destructive", - title: t('apiKeysPermissionsErrorLoadingActions'), + title: t("apiKeysPermissionsErrorLoadingActions"), description: formatAxiosError( e, - t('apiKeysPermissionsErrorLoadingActions') + t("apiKeysPermissionsErrorLoadingActions") ) }); }); @@ -79,18 +79,18 @@ export default function Page() { ) }) .catch((e) => { - console.error(t('apiKeysErrorSetPermission'), e); + console.error(t("apiKeysErrorSetPermission"), e); toast({ variant: "destructive", - title: t('apiKeysErrorSetPermission'), + title: t("apiKeysErrorSetPermission"), description: formatAxiosError(e) }); }); if (actionsRes && actionsRes.status === 200) { toast({ - title: t('apiKeysPermissionsUpdated'), - description: t('apiKeysPermissionsUpdatedDescription') + title: t("apiKeysPermissionsUpdated"), + description: t("apiKeysPermissionsUpdatedDescription") }); } @@ -104,10 +104,12 @@ export default function Page() { - {t('apiKeysPermissionsGeneralSettings')} + {t("apiKeysPermissionsGeneralSettings")} - {t('apiKeysPermissionsGeneralSettingsDescription')} + {t( + "apiKeysPermissionsGeneralSettingsDescription" + )} @@ -124,7 +126,7 @@ export default function Page() { loading={loadingSavePermissions} disabled={loadingSavePermissions} > - {t('apiKeysPermissionsSave')} + {t("apiKeysPermissionsSave")} diff --git a/src/app/[orgId]/settings/api-keys/create/page.tsx b/src/app/[orgId]/settings/api-keys/create/page.tsx index b62c2628..fa062ba1 100644 --- a/src/app/[orgId]/settings/api-keys/create/page.tsx +++ b/src/app/[orgId]/settings/api-keys/create/page.tsx @@ -66,10 +66,10 @@ export default function Page() { name: z .string() .min(2, { - message: t('nameMin', {len: 2}) + message: t("nameMin", { len: 2 }) }) .max(255, { - message: t('nameMax', {len: 255}) + message: t("nameMax", { len: 255 }) }) }); @@ -84,7 +84,7 @@ export default function Page() { return data.copied; }, { - message: t('apiKeysConfirmCopy2'), + message: t("apiKeysConfirmCopy2"), path: ["copied"] } ); @@ -119,7 +119,7 @@ export default function Page() { .catch((e) => { toast({ variant: "destructive", - title: t('apiKeysErrorCreate'), + title: t("apiKeysErrorCreate"), description: formatAxiosError(e) }); }); @@ -140,10 +140,10 @@ export default function Page() { ) }) .catch((e) => { - console.error(t('apiKeysErrorSetPermission'), e); + console.error(t("apiKeysErrorSetPermission"), e); toast({ variant: "destructive", - title: t('apiKeysErrorSetPermission'), + title: t("apiKeysErrorSetPermission"), description: formatAxiosError(e) }); }); @@ -182,8 +182,8 @@ export default function Page() { <>
@@ -203,7 +203,7 @@ export default function Page() { - {t('apiKeysTitle')} + {t("apiKeysTitle")} @@ -224,7 +224,7 @@ export default function Page() { render={({ field }) => ( - {t('name')} + {t("name")} - {t('apiKeysGeneralSettings')} + {t("apiKeysGeneralSettings")} - {t('apiKeysGeneralSettingsDescription')} + {t( + "apiKeysGeneralSettingsDescription" + )} @@ -267,14 +269,14 @@ export default function Page() { - {t('apiKeysList')} + {t("apiKeysList")} - {t('name')} + {t("name")} - {t('created')} + {t("created")} {moment( @@ -297,10 +299,10 @@ export default function Page() { - {t('apiKeysSave')} + {t("apiKeysSave")} - {t('apiKeysSaveDescription')} + {t("apiKeysSaveDescription")} @@ -367,7 +369,7 @@ export default function Page() { router.push(`/${orgId}/settings/api-keys`); }} > - {t('cancel')} + {t("cancel")} )} {!apiKey && ( @@ -379,7 +381,7 @@ export default function Page() { form.handleSubmit(onSubmit)(); }} > - {t('generate')} + {t("generate")} )} @@ -390,7 +392,7 @@ export default function Page() { copiedForm.handleSubmit(onCopiedSubmit)(); }} > - {t('done')} + {t("done")} )} diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx index ca526a7d..2973bb54 100644 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ b/src/app/[orgId]/settings/api-keys/page.tsx @@ -2,9 +2,11 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import OrgApiKeysTable, { OrgApiKeyRow } from "../../../../components/OrgApiKeysTable"; +import OrgApiKeysTable, { + OrgApiKeyRow +} from "../../../../components/OrgApiKeysTable"; import { ListOrgApiKeysResponse } from "@server/routers/apiKeys"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; type ApiKeyPageProps = { params: Promise<{ orgId: string }>; @@ -37,8 +39,8 @@ export default async function ApiKeysPage(props: ApiKeyPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/clients/machine/[clientId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[clientId]/credentials/page.tsx deleted file mode 100644 index 42120319..00000000 --- a/src/app/[orgId]/settings/clients/machine/[clientId]/credentials/page.tsx +++ /dev/null @@ -1,132 +0,0 @@ -"use client"; - -import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { Button } from "@app/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from "@app/components/ui/tooltip"; -import { useClientContext } from "@app/hooks/useClientContext"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient } from "@app/lib/api"; -import { build } from "@server/build"; -import { PickClientDefaultsResponse } from "@server/routers/client"; -import { useTranslations } from "next-intl"; -import { useParams, useRouter } from "next/navigation"; -import { useState } from "react"; - -export default function CredentialsPage() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const { orgId } = useParams(); - const router = useRouter(); - const t = useTranslations(); - const { client } = useClientContext(); - - const [modalOpen, setModalOpen] = useState(false); - const [clientDefaults, setClientDefaults] = - useState(null); - - const { licenseStatus, isUnlocked } = useLicenseStatusContext(); - const subscription = useSubscriptionStatusContext(); - - const isSecurityFeatureDisabled = () => { - const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); - const isSaasNotSubscribed = - build === "saas" && !subscription?.isSubscribed(); - return isEnterpriseNotLicensed || isSaasNotSubscribed; - }; - - const handleConfirmRegenerate = async () => { - const res = await api.get(`/org/${orgId}/pick-client-defaults`); - if (res && res.status === 200) { - const data = res.data.data; - setClientDefaults(data); - - await api.post( - `/re-key/${client?.clientId}/regenerate-client-secret`, - { - olmId: data.olmId, - secret: data.olmSecret - } - ); - - toast({ - title: t("credentialsSaved"), - description: t("credentialsSavedDescription") - }); - - router.refresh(); - } - }; - - const getCredentials = () => { - if (clientDefaults) { - return { - Id: clientDefaults.olmId, - Secret: clientDefaults.olmSecret - }; - } - return undefined; - }; - - return ( - - - - - {t("generatedcredentials")} - - - {t("regenerateCredentials")} - - - - - - - -
- -
-
- - {isSecurityFeatureDisabled() && ( - - {t("featureDisabledTooltip")} - - )} -
-
-
-
- - -
- ); -} diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx new file mode 100644 index 00000000..e75aa3eb --- /dev/null +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx @@ -0,0 +1,257 @@ +"use client"; + +import { useState } from "react"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { Button } from "@app/components/ui/button"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { useParams, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { PickClientDefaultsResponse } from "@server/routers/client"; +import { useClientContext } from "@app/hooks/useClientContext"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { build } from "@server/build"; +import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon } from "lucide-react"; + +export default function CredentialsPage() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); + const router = useRouter(); + const t = useTranslations(); + const { client } = useClientContext(); + + const [modalOpen, setModalOpen] = useState(false); + const [clientDefaults, setClientDefaults] = + useState(null); + const [currentOlmId, setCurrentOlmId] = useState( + client.olmId + ); + const [regeneratedSecret, setRegeneratedSecret] = useState( + null + ); + const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); + const [shouldDisconnect, setShouldDisconnect] = useState(true); + + const { licenseStatus, isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); + + const isSecurityFeatureDisabled = () => { + const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); + const isSaasNotSubscribed = + build === "saas" && !subscription?.isSubscribed(); + return isEnterpriseNotLicensed || isSaasNotSubscribed; + }; + + const handleConfirmRegenerate = async () => { + try { + const res = await api.get(`/org/${orgId}/pick-client-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + + const rekeyRes = await api.post( + `/re-key/${client?.clientId}/regenerate-client-secret`, + { + secret: data.olmSecret, + disconnect: shouldDisconnect + } + ); + + if (rekeyRes && rekeyRes.status === 200) { + const rekeyData = rekeyRes.data.data; + if (rekeyData && rekeyData.olmId) { + setCurrentOlmId(rekeyData.olmId); + setRegeneratedSecret(data.olmSecret); + setClientDefaults({ + ...data, + olmId: rekeyData.olmId + }); + setShowCredentialsAlert(true); + } + } + + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: t("error") || "Error", + description: + formatAxiosError(error) || + t("credentialsRegenerateError") || + "Failed to regenerate credentials" + }); + } + }; + + const getConfirmationString = () => { + return client?.name || client?.clientId?.toString() || "My client"; + }; + + const displayOlmId = currentOlmId || clientDefaults?.olmId || null; + const displaySecret = regeneratedSecret || null; + + return ( + <> + + + + + {t("clientOlmCredentials")} + + + {t("clientOlmCredentialsDescription")} + + + + + + + + + {t("olmEndpoint")} + + + + + + + + {t("olmId")} + + + {displayOlmId ? ( + + ) : ( + {"••••••••••••••••"} + )} + + + + + {t("olmSecretKey")} + + + {displaySecret ? ( + + ) : ( + + {"••••••••••••••••••••••••••••••••"} + + )} + + + + + {showCredentialsAlert && displaySecret && ( + + + + {t("clientCredentialsSave")} + + + {t("clientCredentialsSaveDescription")} + + + )} + + {build !== "oss" && ( + + + + + )} + + + + { + setModalOpen(val); + // Prevent modal from reopening during refresh + if (!val) { + setTimeout(() => { + router.refresh(); + }, 150); + } + }} + dialog={ +
+ {shouldDisconnect ? ( + <> +

+ {t( + "clientRegenerateAndDisconnectConfirmation" + )} +

+

+ {t("clientRegenerateAndDisconnectWarning")} +

+ + ) : ( + <> +

+ {t( + "clientRegenerateCredentialsConfirmation" + )} +

+

{t("clientRegenerateCredentialsWarning")}

+ + )} +
+ } + buttonText={ + shouldDisconnect + ? t("clientRegenerateAndDisconnect") + : t("regenerateCredentialsButton") + } + onConfirm={handleConfirmRegenerate} + string={getConfirmationString()} + title={t("regenerateCredentials")} + warningText={t("cannotbeUndone")} + /> + + ); +} diff --git a/src/app/[orgId]/settings/clients/machine/[clientId]/general/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/general/page.tsx similarity index 77% rename from src/app/[orgId]/settings/clients/machine/[clientId]/general/page.tsx rename to src/app/[orgId]/settings/clients/machine/[niceId]/general/page.tsx index 63d88593..c2ef26e4 100644 --- a/src/app/[orgId]/settings/clients/machine/[clientId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/general/page.tsx @@ -34,7 +34,8 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; const GeneralFormSchema = z.object({ - name: z.string().nonempty("Name is required") + name: z.string().nonempty("Name is required"), + niceId: z.string().min(1).max(255).optional() }); type GeneralFormValues = z.infer; @@ -49,7 +50,8 @@ export default function GeneralPage() { const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { - name: client?.name + name: client?.name, + niceId: client?.niceId || "" }, mode: "onChange" }); @@ -84,10 +86,11 @@ export default function GeneralPage() { try { await api.post(`/client/${client?.clientId}`, { - name: data.name + name: data.name, + niceId: data.niceId }); - updateClient({ name: data.name }); + updateClient({ name: data.name, niceId: data.niceId }); toast({ title: t("clientUpdated"), @@ -139,6 +142,28 @@ export default function GeneralPage() {
)} /> + + ( + + + {t("identifier")} + + + + + + + )} + /> diff --git a/src/app/[orgId]/settings/clients/machine/[clientId]/layout.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/layout.tsx similarity index 77% rename from src/app/[orgId]/settings/clients/machine/[clientId]/layout.tsx rename to src/app/[orgId]/settings/clients/machine/[niceId]/layout.tsx index a51a003d..145fb172 100644 --- a/src/app/[orgId]/settings/clients/machine/[clientId]/layout.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/layout.tsx @@ -4,7 +4,6 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import ClientProvider from "@app/providers/ClientProvider"; -import { build } from "@server/build"; import { GetClientResponse } from "@server/routers/client"; import { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; @@ -12,7 +11,7 @@ import { redirect } from "next/navigation"; type SettingsLayoutProps = { children: React.ReactNode; - params: Promise<{ clientId: number | string; orgId: string }>; + params: Promise<{ niceId: number | string; orgId: string }>; }; export default async function SettingsLayout(props: SettingsLayoutProps) { @@ -22,8 +21,12 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { let client = null; try { + console.log( + "making request to ", + `/org/${params.orgId}/client/${params.niceId}` + ); const res = await internal.get>( - `/client/${params.clientId}`, + `/org/${params.orgId}/client/${params.niceId}`, await authCookieHeader() ); client = res.data.data; @@ -37,16 +40,12 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const navItems = [ { title: t("general"), - href: `/{orgId}/settings/clients/machine/{clientId}/general` + href: `/{orgId}/settings/clients/machine/{niceId}/general` }, - ...(build === "enterprise" - ? [ - { - title: t("credentials"), - href: `/{orgId}/settings/clients/machine/{clientId}/credentials` - } - ] - : []) + { + title: t("credentials"), + href: `/{orgId}/settings/clients/machine/{niceId}/credentials` + } ]; return ( diff --git a/src/app/[orgId]/settings/clients/machine/[clientId]/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/page.tsx similarity index 52% rename from src/app/[orgId]/settings/clients/machine/[clientId]/page.tsx rename to src/app/[orgId]/settings/clients/machine/[niceId]/page.tsx index c59f6920..3aa4a2c4 100644 --- a/src/app/[orgId]/settings/clients/machine/[clientId]/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/page.tsx @@ -1,10 +1,10 @@ import { redirect } from "next/navigation"; export default async function ClientPage(props: { - params: Promise<{ orgId: string; clientId: number | string }>; + params: Promise<{ orgId: string; niceId: number | string }>; }) { const params = await props.params; redirect( - `/${params.orgId}/settings/clients/machine/${params.clientId}/general` + `/${params.orgId}/settings/clients/machine/${params.niceId}/general` ); } diff --git a/src/app/[orgId]/settings/clients/machine/create/page.tsx b/src/app/[orgId]/settings/clients/machine/create/page.tsx index a0489f30..05ace912 100644 --- a/src/app/[orgId]/settings/clients/machine/create/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/create/page.tsx @@ -136,7 +136,7 @@ export default function Page() { All: [ { title: t("install"), - command: `curl -fsSL https://pangolin.net/get-olm.sh | bash` + command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash` }, { title: t("run"), @@ -276,7 +276,7 @@ export default function Page() { if (res && res.status === 201) { const data = res.data.data; - router.push(`/${orgId}/settings/clients/machine/${data.clientId}`); + router.push(`/${orgId}/settings/clients/machine/${data.niceId}`); } setCreateLoading(false); diff --git a/src/app/[orgId]/settings/clients/machine/page.tsx b/src/app/[orgId]/settings/clients/machine/page.tsx index b450b09f..e1a904ad 100644 --- a/src/app/[orgId]/settings/clients/machine/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/page.tsx @@ -56,7 +56,9 @@ export default async function ClientsPage(props: ClientsPageProps) { olmUpdateAvailable: client.olmUpdateAvailable || false, userId: client.userId, username: client.username, - userEmail: client.userEmail + userEmail: client.userEmail, + niceId: client.niceId, + agent: client.agent }; }; diff --git a/src/app/[orgId]/settings/clients/user/page.tsx b/src/app/[orgId]/settings/clients/user/page.tsx index 399588fc..28288fd2 100644 --- a/src/app/[orgId]/settings/clients/user/page.tsx +++ b/src/app/[orgId]/settings/clients/user/page.tsx @@ -53,7 +53,9 @@ export default async function ClientsPage(props: ClientsPageProps) { olmUpdateAvailable: client.olmUpdateAvailable || false, userId: client.userId, username: client.username, - userEmail: client.userEmail + userEmail: client.userEmail, + niceId: client.niceId, + agent: client.agent }; }; diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 82b2c999..8c8efa59 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -10,7 +10,7 @@ import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; type GeneralSettingsProps = { children: React.ReactNode; @@ -19,7 +19,7 @@ type GeneralSettingsProps = { export default async function GeneralSettingsPage({ children, - params, + params }: GeneralSettingsProps) { const { orgId } = await params; @@ -35,8 +35,8 @@ export default async function GeneralSettingsPage({ const getOrgUser = cache(async () => internal.get>( `/org/${orgId}/user/${user.userId}`, - await authCookieHeader(), - ), + await authCookieHeader() + ) ); const res = await getOrgUser(); orgUser = res.data.data; @@ -49,8 +49,8 @@ export default async function GeneralSettingsPage({ const getOrg = cache(async () => internal.get>( `/org/${orgId}`, - await authCookieHeader(), - ), + await authCookieHeader() + ) ); const res = await getOrg(); org = res.data.data; @@ -62,9 +62,9 @@ export default async function GeneralSettingsPage({ const navItems = [ { - title: t('general'), - href: `/{orgId}/settings/general`, - }, + title: t("general"), + href: `/{orgId}/settings/general` + } ]; return ( @@ -72,13 +72,11 @@ export default async function GeneralSettingsPage({ - - {children} - + {children} diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index e991ea70..e391922f 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -102,7 +102,12 @@ const LOG_RETENTION_OPTIONS = [ { label: "logRetention14Days", value: 14 }, { label: "logRetention30Days", value: 30 }, { label: "logRetention90Days", value: 90 }, - ...(build != "saas" ? [{ label: "logRetentionForever", value: -1 }] : []) + ...(build != "saas" + ? [ + { label: "logRetentionForever", value: -1 }, + { label: "logRetentionEndOfFollowingYear", value: 9001 } + ] + : []) ]; export default function GeneralPage() { diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index b2f9bab4..42cfec57 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -6,7 +6,11 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable"; +import { + getStoredPageSize, + LogDataTable, + setStoredPageSize +} from "@app/components/LogDataTable"; import { ColumnDef } from "@tanstack/react-table"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { Key, RouteOff, User, Lock, Unlock, ArrowUpRight } from "lucide-react"; @@ -757,8 +761,8 @@ export default function GeneralPage() { return ( <> {authInfo.headerAuth - ? t("resourceHeaderAuthProtectionEnabled") + ? t( + "resourceHeaderAuthProtectionEnabled" + ) : t( "resourceHeaderAuthProtectionDisabled" )} @@ -921,7 +923,8 @@ export default function ResourceAuthenticationPage() { validateTag={( tag ) => { - return z.email() + return z + .email() .or( z .string() diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 3d9aa977..fa9a6976 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -104,7 +104,7 @@ export default function GeneralForm() { name: z.string().min(1).max(255), niceId: z.string().min(1).max(255).optional(), domainId: z.string().optional(), - proxyPort: z.int().min(1).max(65535).optional(), + proxyPort: z.int().min(1).max(65535).optional() // enableProxy: z.boolean().optional() }) .refine( @@ -134,7 +134,7 @@ export default function GeneralForm() { niceId: resource.niceId, subdomain: resource.subdomain ? resource.subdomain : undefined, domainId: resource.domainId || undefined, - proxyPort: resource.proxyPort || undefined, + proxyPort: resource.proxyPort || undefined // enableProxy: resource.enableProxy || false }, mode: "onChange" @@ -168,7 +168,7 @@ export default function GeneralForm() { const rawDomains = res.data.data.domains as DomainRow[]; const domains = rawDomains.map((domain) => ({ ...domain, - baseDomain: toUnicode(domain.baseDomain), + baseDomain: toUnicode(domain.baseDomain) })); setBaseDomains(domains); setFormKey((key) => key + 1); @@ -195,9 +195,11 @@ export default function GeneralForm() { enabled: data.enabled, name: data.name, niceId: data.niceId, - subdomain: data.subdomain ? toASCII(data.subdomain) : undefined, + subdomain: data.subdomain + ? toASCII(data.subdomain) + : undefined, domainId: data.domainId, - proxyPort: data.proxyPort, + proxyPort: data.proxyPort // ...(!resource.http && { // enableProxy: data.enableProxy // }) @@ -223,7 +225,7 @@ export default function GeneralForm() { niceId: data.niceId, subdomain: data.subdomain, fullDomain: resource.fullDomain, - proxyPort: data.proxyPort, + proxyPort: data.proxyPort // ...(!resource.http && { // enableProxy: data.enableProxy // }) @@ -235,7 +237,9 @@ export default function GeneralForm() { }); if (data.niceId && data.niceId !== resource?.niceId) { - router.replace(`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`); + router.replace( + `/${updated.orgId}/settings/resources/proxy/${data.niceId}/general` + ); } else { router.refresh(); } @@ -320,11 +324,15 @@ export default function GeneralForm() { name="niceId" render={({ field }) => ( - {t("identifier")} + + {t("identifier")} + @@ -360,10 +368,10 @@ export default function GeneralForm() { .target .value ? parseInt( - e - .target - .value - ) + e + .target + .value + ) : undefined ) } @@ -498,17 +506,29 @@ export default function GeneralForm() { ) : ( - @@ -558,7 +557,7 @@ function ProxyResourceTargetsForm({ { const input = e.target.value.trim(); diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx index a464439b..39badea4 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx @@ -114,23 +114,25 @@ export default function ResourceRules(props: { const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules); const [openCountrySelect, setOpenCountrySelect] = useState(false); const [countrySelectValue, setCountrySelectValue] = useState(""); - const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); + const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = + useState(false); const router = useRouter(); const t = useTranslations(); const { env } = useEnvContext(); - const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0; + const isMaxmindAvailable = + env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0; const RuleAction = { - ACCEPT: t('alwaysAllow'), - DROP: t('alwaysDeny'), - PASS: t('passToAuth') + ACCEPT: t("alwaysAllow"), + DROP: t("alwaysDeny"), + PASS: t("passToAuth") } as const; const RuleMatch = { - PATH: t('path'), + PATH: t("path"), IP: "IP", - CIDR: t('ipAddressRange'), - COUNTRY: t('country') + CIDR: t("ipAddressRange"), + COUNTRY: t("country") } as const; const addRuleForm = useForm({ @@ -155,10 +157,10 @@ export default function ResourceRules(props: { console.error(err); toast({ variant: "destructive", - title: t('rulesErrorFetch'), + title: t("rulesErrorFetch"), description: formatAxiosError( err, - t('rulesErrorFetchDescription') + t("rulesErrorFetchDescription") ) }); } finally { @@ -179,8 +181,8 @@ export default function ResourceRules(props: { if (isDuplicate) { toast({ variant: "destructive", - title: t('rulesErrorDuplicate'), - description: t('rulesErrorDuplicateDescription') + title: t("rulesErrorDuplicate"), + description: t("rulesErrorDuplicateDescription") }); return; } @@ -188,8 +190,8 @@ export default function ResourceRules(props: { if (data.match === "CIDR" && !isValidCIDR(data.value)) { toast({ variant: "destructive", - title: t('rulesErrorInvalidIpAddressRange'), - description: t('rulesErrorInvalidIpAddressRangeDescription') + title: t("rulesErrorInvalidIpAddressRange"), + description: t("rulesErrorInvalidIpAddressRangeDescription") }); setLoading(false); return; @@ -197,8 +199,8 @@ export default function ResourceRules(props: { if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { toast({ variant: "destructive", - title: t('rulesErrorInvalidUrl'), - description: t('rulesErrorInvalidUrlDescription') + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") }); setLoading(false); return; @@ -206,17 +208,22 @@ export default function ResourceRules(props: { if (data.match === "IP" && !isValidIP(data.value)) { toast({ variant: "destructive", - title: t('rulesErrorInvalidIpAddress'), - description: t('rulesErrorInvalidIpAddressDescription') + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") }); setLoading(false); return; } - if (data.match === "COUNTRY" && !COUNTRIES.some(c => c.code === data.value)) { + if ( + data.match === "COUNTRY" && + !COUNTRIES.some((c) => c.code === data.value) + ) { toast({ variant: "destructive", - title: t('rulesErrorInvalidCountry'), - description: t('rulesErrorInvalidCountryDescription') || "Invalid country code." + title: t("rulesErrorInvalidCountry"), + description: + t("rulesErrorInvalidCountryDescription") || + "Invalid country code." }); setLoading(false); return; @@ -265,13 +272,13 @@ export default function ResourceRules(props: { function getValueHelpText(type: string) { switch (type) { case "CIDR": - return t('rulesMatchIpAddressRangeDescription'); + return t("rulesMatchIpAddressRangeDescription"); case "IP": - return t('rulesMatchIpAddress'); + return t("rulesMatchIpAddress"); case "PATH": - return t('rulesMatchUrl'); + return t("rulesMatchUrl"); case "COUNTRY": - return t('rulesMatchCountry'); + return t("rulesMatchCountry"); } } @@ -288,10 +295,10 @@ export default function ResourceRules(props: { console.error(err); toast({ variant: "destructive", - title: t('rulesErrorUpdate'), + title: t("rulesErrorUpdate"), description: formatAxiosError( err, - t('rulesErrorUpdateDescription') + t("rulesErrorUpdateDescription") ) }); throw err; @@ -314,8 +321,10 @@ export default function ResourceRules(props: { if (rule.match === "CIDR" && !isValidCIDR(rule.value)) { toast({ variant: "destructive", - title: t('rulesErrorInvalidIpAddressRange'), - description: t('rulesErrorInvalidIpAddressRangeDescription') + title: t("rulesErrorInvalidIpAddressRange"), + description: t( + "rulesErrorInvalidIpAddressRangeDescription" + ) }); setLoading(false); return; @@ -326,8 +335,8 @@ export default function ResourceRules(props: { ) { toast({ variant: "destructive", - title: t('rulesErrorInvalidUrl'), - description: t('rulesErrorInvalidUrlDescription') + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") }); setLoading(false); return; @@ -335,8 +344,8 @@ export default function ResourceRules(props: { if (rule.match === "IP" && !isValidIP(rule.value)) { toast({ variant: "destructive", - title: t('rulesErrorInvalidIpAddress'), - description: t('rulesErrorInvalidIpAddressDescription') + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") }); setLoading(false); return; @@ -345,8 +354,8 @@ export default function ResourceRules(props: { if (rule.priority === undefined) { toast({ variant: "destructive", - title: t('rulesErrorInvalidPriority'), - description: t('rulesErrorInvalidPriorityDescription') + title: t("rulesErrorInvalidPriority"), + description: t("rulesErrorInvalidPriorityDescription") }); setLoading(false); return; @@ -357,8 +366,8 @@ export default function ResourceRules(props: { if (priorities.length !== new Set(priorities).size) { toast({ variant: "destructive", - title: t('rulesErrorDuplicatePriority'), - description: t('rulesErrorDuplicatePriorityDescription') + title: t("rulesErrorDuplicatePriority"), + description: t("rulesErrorDuplicatePriorityDescription") }); setLoading(false); return; @@ -397,8 +406,8 @@ export default function ResourceRules(props: { } toast({ - title: t('ruleUpdated'), - description: t('ruleUpdatedDescription') + title: t("ruleUpdated"), + description: t("ruleUpdatedDescription") }); setRulesToRemove([]); @@ -407,10 +416,10 @@ export default function ResourceRules(props: { console.error(err); toast({ variant: "destructive", - title: t('ruleErrorUpdate'), + title: t("ruleErrorUpdate"), description: formatAxiosError( err, - t('ruleErrorUpdateDescription') + t("ruleErrorUpdateDescription") ) }); } @@ -428,7 +437,7 @@ export default function ResourceRules(props: { column.toggleSorting(column.getIsSorted() === "asc") } > - {t('rulesPriority')} + {t("rulesPriority")} ); @@ -440,15 +449,18 @@ export default function ResourceRules(props: { type="number" onClick={(e) => e.currentTarget.focus()} onBlur={(e) => { - const parsed = z.int() + const parsed = z + .int() .optional() .safeParse(e.target.value); if (!parsed.data) { toast({ variant: "destructive", - title: t('rulesErrorInvalidIpAddress'), // correct priority or IP? - description: t('rulesErrorInvalidPriorityDescription') + title: t("rulesErrorInvalidIpAddress"), // correct priority or IP? + description: t( + "rulesErrorInvalidPriorityDescription" + ) }); setLoading(false); return; @@ -463,7 +475,7 @@ export default function ResourceRules(props: { }, { accessorKey: "action", - header: () => ({t('rulesAction')}), + header: () => {t("rulesAction")}, cell: ({ row }) => ( - updateRule(row.original.ruleId, { match: value, value: value === "COUNTRY" ? "US" : row.original.value }) + onValueChange={( + value: "CIDR" | "IP" | "PATH" | "COUNTRY" + ) => + updateRule(row.original.ruleId, { + match: value, + value: + value === "COUNTRY" ? "US" : row.original.value + }) } > @@ -502,7 +520,9 @@ export default function ResourceRules(props: { {RuleMatch.IP} {RuleMatch.CIDR} {isMaxmindAvailable && ( - {RuleMatch.COUNTRY} + + {RuleMatch.COUNTRY} + )} @@ -510,8 +530,8 @@ export default function ResourceRules(props: { }, { accessorKey: "value", - header: () => ({t('value')}), - cell: ({ row }) => ( + header: () => {t("value")}, + cell: ({ row }) => row.original.match === "COUNTRY" ? ( @@ -521,29 +541,43 @@ export default function ResourceRules(props: { className="min-w-[200px] justify-between" > {row.original.value - ? COUNTRIES.find((country) => country.code === row.original.value)?.name + - " (" + row.original.value + ")" - : t('selectCountry')} + ? COUNTRIES.find( + (country) => + country.code === + row.original.value + )?.name + + " (" + + row.original.value + + ")" + : t("selectCountry")} - + - {t('noCountryFound')} + + {t("noCountryFound")} + {COUNTRIES.map((country) => ( { - updateRule(row.original.ruleId, { value: country.code }); + updateRule( + row.original.ruleId, + { value: country.code } + ); }} > ) - ) }, { accessorKey: "enabled", - header: () => ({t('enabled')}), + header: () => {t("enabled")}, cell: ({ row }) => ( ({t('actions')}), + header: () => {t("actions")}, cell: ({ row }) => (
) @@ -664,10 +697,10 @@ export default function ResourceRules(props: { - {t('rulesResource')} + {t("rulesResource")} - {t('rulesResourceDescription')} + {t("rulesResourceDescription")} @@ -675,7 +708,7 @@ export default function ResourceRules(props: {
setRulesEnabled(val)} /> @@ -692,7 +725,9 @@ export default function ResourceRules(props: { name="action" render={({ field }) => ( - {t('rulesAction')} + + {t("rulesAction")} + @@ -725,11 +766,15 @@ export default function ResourceRules(props: { name="match" render={({ field }) => ( - {t('rulesMatchType')} + + {t("rulesMatchType")} + @@ -1050,7 +1057,7 @@ export default function Page() { { const input = e.target.value.trim(); @@ -1128,7 +1135,7 @@ export default function Page() { const rewritePathColumn: ColumnDef = { accessorKey: "rewritePath", - header: () => ({t("rewritePath")}), + header: () => {t("rewritePath")}, cell: ({ row }) => { const hasRewritePath = !!( row.original.rewritePath || row.original.rewritePathType @@ -1198,7 +1205,7 @@ export default function Page() { const enabledColumn: ColumnDef = { accessorKey: "enabled", - header: () => ({t("enabled")}), + header: () => {t("enabled")}, cell: ({ row }) => (
= { id: "actions", - header: () => ({t("actions")}), + header: () => {t("actions")}, cell: ({ row }) => (
) : ( -
+

{t("targetNoOne")}

diff --git a/src/app/[orgId]/settings/share-links/page.tsx b/src/app/[orgId]/settings/share-links/page.tsx index caf02b83..3b52393c 100644 --- a/src/app/[orgId]/settings/share-links/page.tsx +++ b/src/app/[orgId]/settings/share-links/page.tsx @@ -7,7 +7,9 @@ import { cache } from "react"; import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { ListAccessTokensResponse } from "@server/routers/accessToken"; -import ShareLinksTable, { ShareLinkRow } from "../../../../components/ShareLinksTable"; +import ShareLinksTable, { + ShareLinkRow +} from "../../../../components/ShareLinksTable"; import { getTranslations } from "next-intl/server"; type ShareLinksPageProps = { @@ -58,8 +60,8 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) { {/* */} diff --git a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx index 6dcee413..6258cd69 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx @@ -1,11 +1,12 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -18,11 +19,26 @@ import { useTranslations } from "next-intl"; import { PickSiteDefaultsResponse } from "@server/routers/site"; import { useSiteContext } from "@app/hooks/useSiteContext"; import { generateKeypair } from "../wireguardConfig"; -import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { build } from "@server/build"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon } from "lucide-react"; +import { + generateWireGuardConfig, + generateObfuscatedWireGuardConfig +} from "@app/lib/wireguard"; +import { QRCodeCanvas } from "qrcode.react"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -33,9 +49,20 @@ export default function CredentialsPage() { const { site } = useSiteContext(); const [modalOpen, setModalOpen] = useState(false); - const [siteDefaults, setSiteDefaults] = useState(null); + const [siteDefaults, setSiteDefaults] = + useState(null); const [wgConfig, setWgConfig] = useState(""); const [publicKey, setPublicKey] = useState(""); + const [currentNewtId, setCurrentNewtId] = useState( + site.newtId + ); + const [regeneratedSecret, setRegeneratedSecret] = useState( + null + ); + const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); + const [showWireGuardAlert, setShowWireGuardAlert] = useState(false); + const [loadingDefaults, setLoadingDefaults] = useState(false); + const [shouldDisconnect, setShouldDisconnect] = useState(true); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); @@ -47,147 +74,394 @@ export default function CredentialsPage() { return isEnterpriseNotLicensed || isSaasNotSubscribed; }; - - const hydrateWireGuardConfig = ( - privateKey: string, - publicKey: string, - subnet: string, - address: string, - endpoint: string, - listenPort: string - ) => { - const config = `[Interface] -Address = ${subnet} -ListenPort = 51820 -PrivateKey = ${privateKey} - -[Peer] -PublicKey = ${publicKey} -AllowedIPs = ${address.split("/")[0]}/32 -Endpoint = ${endpoint}:${listenPort} -PersistentKeepalive = 5`; - setWgConfig(config); - return config; - }; + // Fetch site defaults for wireguard sites to show in obfuscated config + useEffect(() => { + const fetchSiteDefaults = async () => { + if (site?.type === "wireguard" && !siteDefaults && orgId) { + setLoadingDefaults(true); + try { + const res = await api.get( + `/org/${orgId}/pick-site-defaults` + ); + if (res && res.status === 200) { + setSiteDefaults(res.data.data); + } + } catch (error) { + // Silently fail - we'll use site data or obfuscated values + } finally { + setLoadingDefaults(false); + } + } else { + setLoadingDefaults(false); + } + }; + fetchSiteDefaults(); + }, []); const handleConfirmRegenerate = async () => { - let generatedPublicKey = ""; - let generatedWgConfig = ""; + try { + let generatedPublicKey = ""; + let generatedWgConfig = ""; - if (site?.type === "wireguard") { - const generatedKeypair = generateKeypair(); - generatedPublicKey = generatedKeypair.publicKey; - setPublicKey(generatedPublicKey); + if (site?.type === "wireguard") { + const generatedKeypair = generateKeypair(); + generatedPublicKey = generatedKeypair.publicKey; + setPublicKey(generatedPublicKey); - const res = await api.get(`/org/${orgId}/pick-site-defaults`); - if (res && res.status === 200) { - const data = res.data.data; - setSiteDefaults(data); + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + setSiteDefaults(data); - // generate config with the fetched data - generatedWgConfig = hydrateWireGuardConfig( - generatedKeypair.privateKey, - data.publicKey, - data.subnet, - data.address, - data.endpoint, - data.listenPort + // generate config with the fetched data + generatedWgConfig = generateWireGuardConfig( + generatedKeypair.privateKey, + data.publicKey, + data.subnet, + data.address, + data.endpoint, + data.listenPort + ); + setWgConfig(generatedWgConfig); + setShowWireGuardAlert(true); + } + + await api.post( + `/re-key/${site?.siteId}/regenerate-site-secret`, + { + type: "wireguard", + pubKey: generatedPublicKey + } ); } - await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, { - type: "wireguard", - subnet: res.data.data.subnet, - exitNodeId: res.data.data.exitNodeId, - pubKey: generatedPublicKey + if (site?.type === "newt") { + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + + const rekeyRes = await api.post( + `/re-key/${site?.siteId}/regenerate-site-secret`, + { + type: "newt", + secret: data.newtSecret, + disconnect: shouldDisconnect + } + ); + + if (rekeyRes && rekeyRes.status === 200) { + const rekeyData = rekeyRes.data.data; + if (rekeyData && rekeyData.newtId) { + setCurrentNewtId(rekeyData.newtId); + setRegeneratedSecret(data.newtSecret); + setSiteDefaults({ + ...data, + newtId: rekeyData.newtId + }); + setShowCredentialsAlert(true); + } + } + } + } + + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + + // ConfirmDeleteDialog handles closing the modal and triggering refresh via setOpen callback + } catch (error) { + toast({ + variant: "destructive", + title: t("error") || "Error", + description: + formatAxiosError(error) || + t("credentialsRegenerateError") || + "Failed to regenerate credentials" }); } - - if (site?.type === "newt") { - const res = await api.get(`/org/${orgId}/pick-site-defaults`); - if (res && res.status === 200) { - const data = res.data.data; - setSiteDefaults(data); - - await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, { - type: "newt", - newtId: data.newtId, - newtSecret: data.newtSecret - }); - } - } - - toast({ - title: t("credentialsSaved"), - description: t("credentialsSavedDescription") - }); - - router.refresh(); }; - const getCredentialType = () => { - if (site?.type === "wireguard") return "site-wireguard"; - if (site?.type === "newt") return "site-newt"; - return "site-newt"; + const getConfirmationString = () => { + return site?.name || site?.niceId || "My site"; }; - const getCredentials = () => { - if (site?.type === "wireguard" && wgConfig) { - return { wgConfig }; - } - if (site?.type === "newt" && siteDefaults) { - return { - Id: siteDefaults.newtId, - Secret: siteDefaults.newtSecret - }; - } - return undefined; - }; + const displayNewtId = currentNewtId || siteDefaults?.newtId || null; + const displaySecret = regeneratedSecret || null; return ( - - - - - {t("generatedcredentials")} - - - {t("regenerateCredentials")} - - + <> + + {site?.type === "newt" && ( + + + + {t("siteNewtCredentials")} + + + {t("siteNewtCredentialsDescription")} + + - - - - -
- -
-
+ - {isSecurityFeatureDisabled() && ( - - {t("featureDisabledTooltip")} - + + + + + {t("newtEndpoint")} + + + + + + + + {t("newtId")} + + + {displayNewtId ? ( + + ) : ( + {"••••••••••••••••"} + )} + + + + + {t("newtSecretKey")} + + + {displaySecret ? ( + + ) : ( + + { + "••••••••••••••••••••••••••••••••" + } + + )} + + + + + {showCredentialsAlert && displaySecret && ( + + + + {t("siteCredentialsSave")} + + + {t("siteCredentialsSaveDescription")} + + )} -
-
-
-
+ + {build !== "oss" && ( + + + + + )} +
+ )} - -
+ {site?.type === "wireguard" && ( + + + + {t("generatedcredentials")} + + + {t("regenerateCredentials")} + + + + + + + {!loadingDefaults && ( + <> + {wgConfig ? ( +
+ +
+
+ +
+
+
+ ) : ( + + )} + {showWireGuardAlert && wgConfig && ( + + + + {t("siteCredentialsSave")} + + + {t( + "siteCredentialsSaveDescription" + )} + + + )} + + )} +
+ {build === "enterprise" && ( + + + + )} +
+ )} + + + {site?.type === "newt" && ( + { + setModalOpen(val); + // Prevent modal from reopening during refresh + if (!val) { + setTimeout(() => { + router.refresh(); + }, 150); + } + }} + dialog={ +
+ {shouldDisconnect ? ( + <> +

+ {t( + "siteRegenerateAndDisconnectConfirmation" + )} +

+

+ {t( + "siteRegenerateAndDisconnectWarning" + )} +

+ + ) : ( + <> +

+ {t( + "siteRegenerateCredentialsConfirmation" + )} +

+

+ {t("siteRegenerateCredentialsWarning")} +

+ + )} +
+ } + buttonText={ + shouldDisconnect + ? t("siteRegenerateAndDisconnect") + : t("regenerateCredentialsButton") + } + onConfirm={handleConfirmRegenerate} + string={getConfirmationString()} + title={t("regenerateCredentials")} + warningText={t("cannotbeUndone")} + /> + )} + + {site?.type === "wireguard" && ( + { + setModalOpen(val); + // Prevent modal from reopening during refresh + if (!val) { + setTimeout(() => { + router.refresh(); + }, 150); + } + }} + dialog={ +
+

{t("regenerateCredentialsConfirmation")}

+

{t("regenerateCredentialsWarning")}

+
+ } + buttonText={t("regenerateCredentialsButton")} + onConfirm={handleConfirmRegenerate} + string={getConfirmationString()} + title={t("regenerateCredentials")} + warningText={t("cannotbeUndone")} + /> + )} + ); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index f0df26c7..1379fc76 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -37,7 +37,7 @@ import Link from "next/link"; const GeneralFormSchema = z.object({ name: z.string().nonempty("Name is required"), niceId: z.string().min(1).max(255).optional(), - dockerSocketEnabled: z.boolean().optional(), + dockerSocketEnabled: z.boolean().optional() }); type GeneralFormValues = z.infer; @@ -52,14 +52,16 @@ export default function GeneralPage() { const { toast } = useToast(); const [loading, setLoading] = useState(false); - const [activeCidrTagIndex, setActiveCidrTagIndex] = useState(null); + const [activeCidrTagIndex, setActiveCidrTagIndex] = useState( + null + ); const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { name: site?.name, niceId: site?.niceId || "", - dockerSocketEnabled: site?.dockerSocketEnabled ?? false, + dockerSocketEnabled: site?.dockerSocketEnabled ?? false }, mode: "onChange" }); @@ -71,17 +73,19 @@ export default function GeneralPage() { await api.post(`/site/${site?.siteId}`, { name: data.name, niceId: data.niceId, - dockerSocketEnabled: data.dockerSocketEnabled, + dockerSocketEnabled: data.dockerSocketEnabled }); updateSite({ name: data.name, niceId: data.niceId, - dockerSocketEnabled: data.dockerSocketEnabled, + dockerSocketEnabled: data.dockerSocketEnabled }); if (data.niceId && data.niceId !== site?.niceId) { - router.replace(`/${site?.orgId}/settings/sites/${data.niceId}/general`); + router.replace( + `/${site?.orgId}/settings/sites/${data.niceId}/general` + ); } toast({ @@ -92,7 +96,10 @@ export default function GeneralPage() { toast({ variant: "destructive", title: t("siteErrorUpdate"), - description: formatAxiosError(e, t("siteErrorUpdateDescription")) + description: formatAxiosError( + e, + t("siteErrorUpdateDescription") + ) }); } @@ -140,11 +147,15 @@ export default function GeneralPage() { name="niceId" render={({ field }) => ( - {t("identifier")} + + {t("identifier")} + diff --git a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx index 8ef00410..30f9eff8 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx @@ -35,25 +35,24 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const navItems = [ { - title: t('general'), - href: `/${params.orgId}/settings/sites/${params.niceId}/general`, + title: t("general"), + href: `/${params.orgId}/settings/sites/${params.niceId}/general` }, - ...(site.type !== 'local' && build === 'enterprise' + ...(site.type !== "local" ? [ - { - title: t('credentials'), - href: `/${params.orgId}/settings/sites/${params.niceId}/credentials`, - }, - ] - : []), + { + title: t("credentials"), + href: `/${params.orgId}/settings/sites/${params.niceId}/credentials` + } + ] + : []) ]; - return ( <> diff --git a/src/app/[orgId]/settings/sites/[niceId]/wireguardConfig.ts b/src/app/[orgId]/settings/sites/[niceId]/wireguardConfig.ts index 01157a68..5e3d1281 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/wireguardConfig.ts +++ b/src/app/[orgId]/settings/sites/[niceId]/wireguardConfig.ts @@ -6,16 +6,16 @@ function gf(init: number[] | undefined = undefined) { var r = new Float64Array(16); if (init) { - for (var i = 0; i < init.length; ++i) - r[i] = init[i]; + for (var i = 0; i < init.length; ++i) r[i] = init[i]; } return r; } function pack(o: Uint8Array, n: Float64Array) { - var b, m = gf(), t = gf(); - for (var i = 0; i < 16; ++i) - t[i] = n[i]; + var b, + m = gf(), + t = gf(); + for (var i = 0; i < 16; ++i) t[i] = n[i]; carry(t); carry(t); carry(t); @@ -45,7 +45,8 @@ function carry(o: Float64Array) { } function cswap(p: Float64Array, q: Float64Array, b: number) { - var t, c = ~(b - 1); + var t, + c = ~(b - 1); for (var i = 0; i < 16; ++i) { t = c & (p[i] ^ q[i]); p[i] ^= t; @@ -54,40 +55,32 @@ function cswap(p: Float64Array, q: Float64Array, b: number) { } function add(o: Float64Array, a: Float64Array, b: Float64Array) { - for (var i = 0; i < 16; ++i) - o[i] = (a[i] + b[i]) | 0; + for (var i = 0; i < 16; ++i) o[i] = (a[i] + b[i]) | 0; } function subtract(o: Float64Array, a: Float64Array, b: Float64Array) { - for (var i = 0; i < 16; ++i) - o[i] = (a[i] - b[i]) | 0; + for (var i = 0; i < 16; ++i) o[i] = (a[i] - b[i]) | 0; } function multmod(o: Float64Array, a: Float64Array, b: Float64Array) { var t = new Float64Array(31); for (var i = 0; i < 16; ++i) { - for (var j = 0; j < 16; ++j) - t[i + j] += a[i] * b[j]; + for (var j = 0; j < 16; ++j) t[i + j] += a[i] * b[j]; } - for (var i = 0; i < 15; ++i) - t[i] += 38 * t[i + 16]; - for (var i = 0; i < 16; ++i) - o[i] = t[i]; + for (var i = 0; i < 15; ++i) t[i] += 38 * t[i + 16]; + for (var i = 0; i < 16; ++i) o[i] = t[i]; carry(o); carry(o); } function invert(o: Float64Array, i: Float64Array) { var c = gf(); - for (var a = 0; a < 16; ++a) - c[a] = i[a]; + for (var a = 0; a < 16; ++a) c[a] = i[a]; for (var a = 253; a >= 0; --a) { multmod(c, c, c); - if (a !== 2 && a !== 4) - multmod(c, c, i); + if (a !== 2 && a !== 4) multmod(c, c, i); } - for (var a = 0; a < 16; ++a) - o[a] = c[a]; + for (var a = 0; a < 16; ++a) o[a] = c[a]; } function clamp(z: Uint8Array) { @@ -96,7 +89,8 @@ function clamp(z: Uint8Array) { } function generatePublicKey(privateKey: Uint8Array) { - var r, z = new Uint8Array(32); + var r, + z = new Uint8Array(32); var a = gf([1]), b = gf([9]), c = gf(), @@ -105,8 +99,7 @@ function generatePublicKey(privateKey: Uint8Array) { f = gf(), _121665 = gf([0xdb41, 1]), _9 = gf([9]); - for (var i = 0; i < 32; ++i) - z[i] = privateKey[i]; + for (var i = 0; i < 32; ++i) z[i] = privateKey[i]; clamp(z); for (var i = 254; i >= 0; --i) { r = (z[i >>> 3] >>> (i & 7)) & 1; @@ -152,9 +145,16 @@ function generatePrivateKey() { } function encodeBase64(dest: Uint8Array, src: Uint8Array) { - var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]); + var input = Uint8Array.from([ + (src[0] >> 2) & 63, + ((src[0] << 4) | (src[1] >> 4)) & 63, + ((src[1] << 2) | (src[2] >> 6)) & 63, + src[2] & 63 + ]); for (var i = 0; i < 4; ++i) - dest[i] = input[i] + 65 + + dest[i] = + input[i] + + 65 + (((25 - input[i]) >> 8) & 6) - (((51 - input[i]) >> 8) & 75) - (((61 - input[i]) >> 8) & 15) + @@ -162,10 +162,14 @@ function encodeBase64(dest: Uint8Array, src: Uint8Array) { } function keyToBase64(key: Uint8Array) { - var i, base64 = new Uint8Array(44); + var i, + base64 = new Uint8Array(44); for (i = 0; i < 32 / 3; ++i) encodeBase64(base64.subarray(i * 4), key.subarray(i * 3)); - encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0])); + encodeBase64( + base64.subarray(i * 4), + Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]) + ); base64[43] = 61; return String.fromCharCode.apply(null, base64 as any); } @@ -177,4 +181,4 @@ export function generateKeypair() { publicKey: keyToBase64(publicKey), privateKey: keyToBase64(privateKey) }; -} \ 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 a1362e87..6395c265 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -47,6 +47,7 @@ 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"; +import { generateWireGuardConfig } from "@app/lib/wireguard"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { CreateSiteBody, @@ -214,27 +215,6 @@ export default function Page() { string | undefined >(); - const hydrateWireGuardConfig = ( - privateKey: string, - publicKey: string, - subnet: string, - address: string, - endpoint: string, - listenPort: string - ) => { - const wgConfig = `[Interface] -Address = ${subnet} -ListenPort = 51820 -PrivateKey = ${privateKey} - -[Peer] -PublicKey = ${publicKey} -AllowedIPs = ${address.split("/")[0]}/32 -Endpoint = ${endpoint}:${listenPort} -PersistentKeepalive = 5`; - setWgConfig(wgConfig); - }; - const hydrateCommands = ( id: string, secret: string, @@ -252,7 +232,7 @@ PersistentKeepalive = 5`; All: [ { title: t("install"), - command: `curl -fsSL https://pangolin.net/get-newt.sh | bash` + command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash` }, { title: t("run"), @@ -595,7 +575,7 @@ WantedBy=default.target` acceptClients ); - hydrateWireGuardConfig( + const wgConfig = generateWireGuardConfig( privateKey, data.publicKey, data.subnet, @@ -603,6 +583,7 @@ WantedBy=default.target` data.endpoint, data.listenPort ); + setWgConfig(wgConfig); setTunnelTypes((prev: any) => { return prev.map((item: any) => { @@ -771,7 +752,9 @@ WantedBy=default.target` {tunnelTypes.length > 1 && ( <>
- {t("type")} + + {t("type")} +
= 1024 * 1024) { - return t('terabytes', {count: (mb / (1024 * 1024)).toFixed(2)}); + return t("terabytes", { count: (mb / (1024 * 1024)).toFixed(2) }); } else if (mb >= 1024) { - return t('gigabytes', {count: (mb / 1024).toFixed(2)}); + return t("gigabytes", { count: (mb / 1024).toFixed(2) }); } else { - return t('megabytes', {count: mb.toFixed(2)}); + return t("megabytes", { count: mb.toFixed(2) }); } } @@ -53,7 +53,7 @@ export default async function SitesPage(props: SitesPageProps) { newtVersion: site.newtVersion || undefined, newtUpdateAvailable: site.newtUpdateAvailable || false, exitNodeName: site.exitNodeName || undefined, - exitNodeEndpoint: site.exitNodeEndpoint || undefined, + exitNodeEndpoint: site.exitNodeEndpoint || undefined }; }); @@ -62,8 +62,8 @@ export default async function SitesPage(props: SitesPageProps) { {/* */} diff --git a/src/app/admin/api-keys/[apiKeyId]/layout.tsx b/src/app/admin/api-keys/[apiKeyId]/layout.tsx index 7e9e579f..d547a0b0 100644 --- a/src/app/admin/api-keys/[apiKeyId]/layout.tsx +++ b/src/app/admin/api-keys/[apiKeyId]/layout.tsx @@ -34,14 +34,16 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const navItems = [ { - title: t('apiKeysPermissionsTitle'), + title: t("apiKeysPermissionsTitle"), href: "/admin/api-keys/{apiKeyId}/permissions" } ]; return ( <> - + {children} diff --git a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx index e00ae425..fad36194 100644 --- a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx +++ b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx @@ -45,10 +45,10 @@ export default function Page() { .catch((e) => { toast({ variant: "destructive", - title: t('apiKeysPermissionsErrorLoadingActions'), + title: t("apiKeysPermissionsErrorLoadingActions"), description: formatAxiosError( e, - t('apiKeysPermissionsErrorLoadingActions') + t("apiKeysPermissionsErrorLoadingActions") ) }); }); @@ -79,18 +79,18 @@ export default function Page() { ) }) .catch((e) => { - console.error(t('apiKeysErrorSetPermission'), e); + console.error(t("apiKeysErrorSetPermission"), e); toast({ variant: "destructive", - title: t('apiKeysErrorSetPermission'), + title: t("apiKeysErrorSetPermission"), description: formatAxiosError(e) }); }); if (actionsRes && actionsRes.status === 200) { toast({ - title: t('apiKeysPermissionsUpdated'), - description: t('apiKeysPermissionsUpdatedDescription') + title: t("apiKeysPermissionsUpdated"), + description: t("apiKeysPermissionsUpdatedDescription") }); } @@ -104,10 +104,12 @@ export default function Page() { - {t('apiKeysPermissionsTitle')} + {t("apiKeysPermissionsTitle")} - {t('apiKeysPermissionsGeneralSettingsDescription')} + {t( + "apiKeysPermissionsGeneralSettingsDescription" + )} @@ -125,7 +127,7 @@ export default function Page() { loading={loadingSavePermissions} disabled={loadingSavePermissions} > - {t('apiKeysPermissionsSave')} + {t("apiKeysPermissionsSave")} diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx index 65f8e46a..083ec89d 100644 --- a/src/app/admin/api-keys/create/page.tsx +++ b/src/app/admin/api-keys/create/page.tsx @@ -30,7 +30,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; -import { useRouter } from "next/navigation"; +import { useRouter } from "next/navigation"; import { CreateOrgApiKeyBody, CreateOrgApiKeyResponse @@ -64,10 +64,10 @@ export default function Page() { name: z .string() .min(2, { - message: t('nameMin', {len: 2}) + message: t("nameMin", { len: 2 }) }) .max(255, { - message: t('nameMax', {len: 255}) + message: t("nameMax", { len: 255 }) }) }); @@ -82,7 +82,7 @@ export default function Page() { return data.copied; }, { - message: t('apiKeysConfirmCopy2'), + message: t("apiKeysConfirmCopy2"), path: ["copied"] } ); @@ -115,7 +115,7 @@ export default function Page() { .catch((e) => { toast({ variant: "destructive", - title: t('apiKeysErrorCreate'), + title: t("apiKeysErrorCreate"), description: formatAxiosError(e) }); }); @@ -136,10 +136,10 @@ export default function Page() { ) }) .catch((e) => { - console.error(t('apiKeysErrorSetPermission'), e); + console.error(t("apiKeysErrorSetPermission"), e); toast({ variant: "destructive", - title: t('apiKeysErrorSetPermission'), + title: t("apiKeysErrorSetPermission"), description: formatAxiosError(e) }); }); @@ -172,8 +172,8 @@ export default function Page() { <>
@@ -193,7 +193,7 @@ export default function Page() { - {t('apiKeysTitle')} + {t("apiKeysTitle")} @@ -214,7 +214,7 @@ export default function Page() { render={({ field }) => ( - {t('name')} + {t("name")} - {t('apiKeysGeneralSettings')} + {t("apiKeysGeneralSettings")} - {t('apiKeysGeneralSettingsDescription')} + {t( + "apiKeysGeneralSettingsDescription" + )} @@ -258,14 +260,14 @@ export default function Page() { - {t('apiKeysList')} + {t("apiKeysList")} - {t('name')} + {t("name")} - {t('created')} + {t("created")} {moment( @@ -288,10 +290,10 @@ export default function Page() { - {t('apiKeysSave')} + {t("apiKeysSave")} - {t('apiKeysSaveDescription')} + {t("apiKeysSaveDescription")} @@ -358,7 +360,7 @@ export default function Page() { router.push(`/admin/api-keys`); }} > - {t('cancel')} + {t("cancel")} )} {!apiKey && ( @@ -370,7 +372,7 @@ export default function Page() { form.handleSubmit(onSubmit)(); }} > - {t('generate')} + {t("generate")} )} @@ -381,7 +383,7 @@ export default function Page() { copiedForm.handleSubmit(onCopiedSubmit)(); }} > - {t('done')} + {t("done")} )}
diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx index e518911f..60195c4a 100644 --- a/src/app/admin/api-keys/page.tsx +++ b/src/app/admin/api-keys/page.tsx @@ -34,8 +34,8 @@ export default async function ApiKeysPage(props: ApiKeyPageProps) { return ( <> diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 7eae6950..d431efa2 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -58,17 +58,17 @@ export default function GeneralPage() { const t = useTranslations(); const GeneralFormSchema = z.object({ - name: z.string().min(2, { message: t('nameMin', {len: 2}) }), - clientId: z.string().min(1, { message: t('idpClientIdRequired') }), - clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), - authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }), - tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }), - identifierPath: z + name: z.string().min(2, { message: t("nameMin", { len: 2 }) }), + clientId: z.string().min(1, { message: t("idpClientIdRequired") }), + clientSecret: z .string() - .min(1, { message: t('idpPathRequired') }), + .min(1, { message: t("idpClientSecretRequired") }), + authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }), + tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }), + identifierPath: z.string().min(1, { message: t("idpPathRequired") }), emailPath: z.string().optional(), namePath: z.string().optional(), - scopes: z.string().min(1, { message: t('idpScopeRequired') }), + scopes: z.string().min(1, { message: t("idpScopeRequired") }), autoProvision: z.boolean().default(false) }); @@ -111,7 +111,7 @@ export default function GeneralPage() { } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -145,14 +145,14 @@ export default function GeneralPage() { if (res.status === 200) { toast({ - title: t('success'), - description: t('idpUpdatedDescription') + title: t("success"), + description: t("idpUpdatedDescription") }); router.refresh(); } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -171,17 +171,17 @@ export default function GeneralPage() { - {t('idpTitle')} + {t("idpTitle")} - {t('idpSettingsDescription')} + {t("idpSettingsDescription")} - {t('redirectUrl')} + {t("redirectUrl")} @@ -192,10 +192,10 @@ export default function GeneralPage() { - {t('redirectUrlAbout')} + {t("redirectUrlAbout")} - {t('redirectUrlAboutDescription')} + {t("redirectUrlAboutDescription")} @@ -210,12 +210,14 @@ export default function GeneralPage() { name="name" render={({ field }) => ( - {t('name')} + + {t("name")} + - {t('idpDisplayName')} + {t("idpDisplayName")} @@ -225,7 +227,7 @@ export default function GeneralPage() {
- {t('idpAutoProvisionUsersDescription')} + {t("idpAutoProvisionUsersDescription")} @@ -250,10 +252,10 @@ export default function GeneralPage() { - {t('idpOidcConfigure')} + {t("idpOidcConfigure")} - {t('idpOidcConfigureDescription')} + {t("idpOidcConfigureDescription")} @@ -270,13 +272,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpClientId')} + {t("idpClientId")} - {t('idpClientIdDescription')} + {t( + "idpClientIdDescription" + )} @@ -289,7 +293,7 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpClientSecret')} + {t("idpClientSecret")} - {t('idpClientSecretDescription')} + {t( + "idpClientSecretDescription" + )} @@ -311,13 +317,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpAuthUrl')} + {t("idpAuthUrl")} - {t('idpAuthUrlDescription')} + {t( + "idpAuthUrlDescription" + )} @@ -330,13 +338,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpTokenUrl')} + {t("idpTokenUrl")} - {t('idpTokenUrlDescription')} + {t( + "idpTokenUrlDescription" + )} @@ -351,10 +361,10 @@ export default function GeneralPage() { - {t('idpToken')} + {t("idpToken")} - {t('idpTokenDescription')} + {t("idpTokenDescription")} @@ -368,17 +378,21 @@ export default function GeneralPage() { - {t('idpJmespathAbout')} + {t("idpJmespathAbout")} - {t('idpJmespathAboutDescription')} + {t( + "idpJmespathAboutDescription" + )} - {t('idpJmespathAboutDescriptionLink')}{" "} + {t( + "idpJmespathAboutDescriptionLink" + )}{" "} @@ -390,13 +404,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpJmespathLabel')} + {t("idpJmespathLabel")} - {t('idpJmespathLabelDescription')} + {t( + "idpJmespathLabelDescription" + )} @@ -409,13 +425,17 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpJmespathEmailPathOptional')} + {t( + "idpJmespathEmailPathOptional" + )} - {t('idpJmespathEmailPathOptionalDescription')} + {t( + "idpJmespathEmailPathOptionalDescription" + )} @@ -428,13 +448,17 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpJmespathNamePathOptional')} + {t( + "idpJmespathNamePathOptional" + )} - {t('idpJmespathNamePathOptionalDescription')} + {t( + "idpJmespathNamePathOptionalDescription" + )} @@ -447,13 +471,17 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpOidcConfigureScopes')} + {t( + "idpOidcConfigureScopes" + )} - {t('idpOidcConfigureScopesDescription')} + {t( + "idpOidcConfigureScopesDescription" + )} @@ -474,7 +502,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - {t('saveGeneralSettings')} + {t("saveGeneralSettings")}
diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index af64e440..d3d9cb2e 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -30,11 +30,11 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const navItems: HorizontalTabs = [ { - title: t('general'), + title: t("general"), href: `/admin/idp/${params.idpId}/general` }, { - title: t('orgPolicies'), + title: t("orgPolicies"), href: `/admin/idp/${params.idpId}/policies` } ]; @@ -42,8 +42,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <>
diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index e3b085b4..bf17abe9 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -89,7 +89,7 @@ export default function PoliciesPage() { const [editingPolicy, setEditingPolicy] = useState(null); const policyFormSchema = z.object({ - orgId: z.string().min(1, { message: t('orgRequired') }), + orgId: z.string().min(1, { message: t("orgRequired") }), roleMapping: z.string().optional(), orgMapping: z.string().optional() }); @@ -133,7 +133,7 @@ export default function PoliciesPage() { } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -148,7 +148,7 @@ export default function PoliciesPage() { } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -167,7 +167,7 @@ export default function PoliciesPage() { } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -202,15 +202,15 @@ export default function PoliciesPage() { }; setPolicies([...policies, newPolicy]); toast({ - title: t('success'), - description: t('orgPolicyAddedDescription') + title: t("success"), + description: t("orgPolicyAddedDescription") }); setShowAddDialog(false); form.reset(); } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -244,8 +244,8 @@ export default function PoliciesPage() { ) ); toast({ - title: t('success'), - description: t('orgPolicyUpdatedDescription') + title: t("success"), + description: t("orgPolicyUpdatedDescription") }); setShowAddDialog(false); setEditingPolicy(null); @@ -253,7 +253,7 @@ export default function PoliciesPage() { } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -271,13 +271,13 @@ export default function PoliciesPage() { policies.filter((policy) => policy.orgId !== orgId) ); toast({ - title: t('success'), - description: t('orgPolicyDeletedDescription') + title: t("success"), + description: t("orgPolicyDeletedDescription") }); } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -295,13 +295,13 @@ export default function PoliciesPage() { }); if (res.status === 200) { toast({ - title: t('success'), - description: t('defaultMappingsUpdatedDescription') + title: t("success"), + description: t("defaultMappingsUpdatedDescription") }); } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -320,18 +320,18 @@ export default function PoliciesPage() { - {t('orgPoliciesAbout')} + {t("orgPoliciesAbout")} {/*TODO(vlalx): Validate replacing */} - {t('orgPoliciesAboutDescription')}{" "} + {t("orgPoliciesAboutDescription")}{" "} - {t('orgPoliciesAboutDescriptionLink')} + {t("orgPoliciesAboutDescriptionLink")} @@ -340,10 +340,10 @@ export default function PoliciesPage() { - {t('defaultMappingsOptional')} + {t("defaultMappingsOptional")} - {t('defaultMappingsOptionalDescription')} + {t("defaultMappingsOptionalDescription")} @@ -362,13 +362,15 @@ export default function PoliciesPage() { render={({ field }) => ( - {t('defaultMappingsRole')} + {t("defaultMappingsRole")} - {t('defaultMappingsRoleDescription')} + {t( + "defaultMappingsRoleDescription" + )} @@ -381,13 +383,15 @@ export default function PoliciesPage() { render={({ field }) => ( - {t('defaultMappingsOrg')} + {t("defaultMappingsOrg")} - {t('defaultMappingsOrgDescription')} + {t( + "defaultMappingsOrgDescription" + )} @@ -402,7 +406,7 @@ export default function PoliciesPage() { form="policy-default-mappings-form" loading={updateDefaultMappingsLoading} > - {t('defaultMappingsSubmit')} + {t("defaultMappingsSubmit")} @@ -445,11 +449,11 @@ export default function PoliciesPage() { {editingPolicy - ? t('orgPoliciesEdit') - : t('orgPoliciesAdd')} + ? t("orgPoliciesEdit") + : t("orgPoliciesAdd")} - {t('orgPolicyConfig')} + {t("orgPolicyConfig")} @@ -466,7 +470,7 @@ export default function PoliciesPage() { name="orgId" render={({ field }) => ( - {t('org')} + {t("org")} {editingPolicy ? ( ) : ( @@ -490,17 +494,25 @@ export default function PoliciesPage() { org.orgId === field.value )?.name - : t('orgSelect')} + : t( + "orgSelect" + )} - + - {t('orgNotFound')} + {t( + "orgNotFound" + )} {organizations.map( @@ -551,13 +563,15 @@ export default function PoliciesPage() { render={({ field }) => ( - {t('roleMappingPathOptional')} + {t("roleMappingPathOptional")} - {t('defaultMappingsRoleDescription')} + {t( + "defaultMappingsRoleDescription" + )} @@ -570,13 +584,15 @@ export default function PoliciesPage() { render={({ field }) => ( - {t('orgMappingPathOptional')} + {t("orgMappingPathOptional")} - {t('defaultMappingsOrgDescription')} + {t( + "defaultMappingsOrgDescription" + )} @@ -603,7 +619,9 @@ export default function PoliciesPage() { : addPolicyLoading } > - {editingPolicy ? t('orgPolicyUpdate') : t('orgPolicyAdd')} + {editingPolicy + ? t("orgPolicyUpdate") + : t("orgPolicyAdd")} diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 73d605a1..dbcd5f00 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -48,18 +48,18 @@ export default function Page() { const t = useTranslations(); const createIdpFormSchema = z.object({ - name: z.string().min(2, { message: t('nameMin', {len: 2}) }), + name: z.string().min(2, { message: t("nameMin", { len: 2 }) }), type: z.enum(["oidc"]), - clientId: z.string().min(1, { message: t('idpClientIdRequired') }), - clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), - authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }), - tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }), - identifierPath: z + clientId: z.string().min(1, { message: t("idpClientIdRequired") }), + clientSecret: z .string() - .min(1, { message: t('idpPathRequired') }), + .min(1, { message: t("idpClientSecretRequired") }), + authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }), + tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }), + identifierPath: z.string().min(1, { message: t("idpPathRequired") }), emailPath: z.string().optional(), namePath: z.string().optional(), - scopes: z.string().min(1, { message: t('idpScopeRequired') }), + scopes: z.string().min(1, { message: t("idpScopeRequired") }), autoProvision: z.boolean().default(false) }); @@ -75,7 +75,7 @@ export default function Page() { { id: "oidc", title: "OAuth2/OIDC", - description: t('idpOidcDescription') + description: t("idpOidcDescription") } ]; @@ -117,14 +117,14 @@ export default function Page() { if (res.status === 201) { toast({ - title: t('success'), - description: t('idpCreatedDescription') + title: t("success"), + description: t("idpCreatedDescription") }); router.push(`/admin/idp/${res.data.data.idpId}`); } } catch (e) { toast({ - title: t('error'), + title: t("error"), description: formatAxiosError(e), variant: "destructive" }); @@ -137,8 +137,8 @@ export default function Page() { <>
@@ -154,10 +154,10 @@ export default function Page() { - {t('idpTitle')} + {t("idpTitle")} - {t('idpCreateSettingsDescription')} + {t("idpCreateSettingsDescription")} @@ -173,12 +173,14 @@ export default function Page() { name="name" render={({ field }) => ( - {t('name')} + + {t("name")} + - {t('idpDisplayName')} + {t("idpDisplayName")} @@ -188,7 +190,7 @@ export default function Page() {
- {t('idpAutoProvisionUsersDescription')} + {t("idpAutoProvisionUsersDescription")} @@ -212,10 +214,10 @@ export default function Page() { - {t('idpType')} + {t("idpType")} - {t('idpTypeDescription')} + {t("idpTypeDescription")} @@ -235,10 +237,10 @@ export default function Page() { - {t('idpOidcConfigure')} + {t("idpOidcConfigure")} - {t('idpOidcConfigureDescription')} + {t("idpOidcConfigureDescription")} @@ -254,13 +256,15 @@ export default function Page() { render={({ field }) => ( - {t('idpClientId')} + {t("idpClientId")} - {t('idpClientIdDescription')} + {t( + "idpClientIdDescription" + )} @@ -273,7 +277,7 @@ export default function Page() { render={({ field }) => ( - {t('idpClientSecret')} + {t("idpClientSecret")} - {t('idpClientSecretDescription')} + {t( + "idpClientSecretDescription" + )} @@ -295,7 +301,7 @@ export default function Page() { render={({ field }) => ( - {t('idpAuthUrl')} + {t("idpAuthUrl")} - {t('idpAuthUrlDescription')} + {t( + "idpAuthUrlDescription" + )} @@ -317,7 +325,7 @@ export default function Page() { render={({ field }) => ( - {t('idpTokenUrl')} + {t("idpTokenUrl")} - {t('idpTokenUrlDescription')} + {t( + "idpTokenUrlDescription" + )} @@ -338,10 +348,10 @@ export default function Page() { - {t('idpOidcConfigureAlert')} + {t("idpOidcConfigureAlert")} - {t('idpOidcConfigureAlertDescription')} + {t("idpOidcConfigureAlertDescription")} @@ -350,10 +360,10 @@ export default function Page() { - {t('idpToken')} + {t("idpToken")} - {t('idpTokenDescription')} + {t("idpTokenDescription")} @@ -366,17 +376,21 @@ export default function Page() { - {t('idpJmespathAbout')} + {t("idpJmespathAbout")} - {t('idpJmespathAboutDescription')}{" "} + {t( + "idpJmespathAboutDescription" + )}{" "} - {t('idpJmespathAboutDescriptionLink')}{" "} + {t( + "idpJmespathAboutDescriptionLink" + )}{" "} @@ -388,13 +402,15 @@ export default function Page() { render={({ field }) => ( - {t('idpJmespathLabel')} + {t("idpJmespathLabel")} - {t('idpJmespathLabelDescription')} + {t( + "idpJmespathLabelDescription" + )} @@ -407,13 +423,17 @@ export default function Page() { render={({ field }) => ( - {t('idpJmespathEmailPathOptional')} + {t( + "idpJmespathEmailPathOptional" + )} - {t('idpJmespathEmailPathOptionalDescription')} + {t( + "idpJmespathEmailPathOptionalDescription" + )} @@ -426,13 +446,17 @@ export default function Page() { render={({ field }) => ( - {t('idpJmespathNamePathOptional')} + {t( + "idpJmespathNamePathOptional" + )} - {t('idpJmespathNamePathOptionalDescription')} + {t( + "idpJmespathNamePathOptionalDescription" + )} @@ -445,13 +469,17 @@ export default function Page() { render={({ field }) => ( - {t('idpOidcConfigureScopes')} + {t( + "idpOidcConfigureScopes" + )} - {t('idpOidcConfigureScopesDescription')} + {t( + "idpOidcConfigureScopesDescription" + )} @@ -473,7 +501,7 @@ export default function Page() { router.push("/admin/idp"); }} > - {t('cancel')} + {t("cancel")}
diff --git a/src/app/admin/idp/page.tsx b/src/app/admin/idp/page.tsx index fef0990c..a341c046 100644 --- a/src/app/admin/idp/page.tsx +++ b/src/app/admin/idp/page.tsx @@ -22,8 +22,8 @@ export default async function IdpPage() { return ( <> diff --git a/src/app/admin/license/layout.tsx b/src/app/admin/license/layout.tsx index 6c6e8baf..56813034 100644 --- a/src/app/admin/license/layout.tsx +++ b/src/app/admin/license/layout.tsx @@ -14,4 +14,3 @@ export default async function AdminLicenseLayout(props: LayoutProps) { return props.children; } - diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index d01b215a..ac6d3e67 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -316,9 +316,7 @@ export default function LicensePage() { }} dialog={
-

- {t("licenseQuestionRemove")} -

+

{t("licenseQuestionRemove")}

{t("licenseMessageRemove")}

diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index 94f3f0b9..efcf9484 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -188,7 +188,7 @@ export default function UsersTable({ users }: Props) { }, { id: "actions", - header: () => ({t("actions")}), + header: () => {t("actions")}, cell: ({ row }) => { const r = row.original; return ( diff --git a/src/app/admin/users/[userId]/layout.tsx b/src/app/admin/users/[userId]/layout.tsx index 062b40d8..0c8c50e6 100644 --- a/src/app/admin/users/[userId]/layout.tsx +++ b/src/app/admin/users/[userId]/layout.tsx @@ -6,7 +6,7 @@ import { AdminGetUserResponse } from "@server/routers/user/adminGetUser"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { cache } from "react"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; interface UserLayoutProps { children: React.ReactNode; @@ -36,7 +36,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) { const navItems = [ { - title: t('general'), + title: t("general"), href: "/admin/users/{userId}/general" } ]; @@ -45,11 +45,9 @@ export default async function UserLayoutProps(props: UserLayoutProps) { <> - - {children} - + {children} ); -} \ No newline at end of file +} diff --git a/src/app/admin/users/[userId]/page.tsx b/src/app/admin/users/[userId]/page.tsx index edf5aaed..07b39686 100644 --- a/src/app/admin/users/[userId]/page.tsx +++ b/src/app/admin/users/[userId]/page.tsx @@ -5,4 +5,4 @@ export default async function UserPage(props: { }) { const { userId } = await props.params; redirect(`/admin/users/${userId}/general`); -} \ No newline at end of file +} diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index bf6547a3..c84a077f 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -36,7 +36,7 @@ export default async function UsersPage(props: PageProps) { username: row.username, type: row.type, idpId: row.idpId, - idpName: row.idpName || t('idpNameInternal'), + idpName: row.idpName || t("idpNameInternal"), dateCreated: row.dateCreated, serverAdmin: row.serverAdmin, twoFactorEnabled: row.twoFactorEnabled, @@ -47,14 +47,16 @@ export default async function UsersPage(props: PageProps) { return ( <> - {t('userAbount')} + + {t("userAbount")} + - {t('userAbountDescription')} + {t("userAbountDescription")} diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/(private)/org/page.tsx index 36dfd175..5b68708a 100644 --- a/src/app/auth/(private)/org/page.tsx +++ b/src/app/auth/(private)/org/page.tsx @@ -9,9 +9,7 @@ import { LoginFormIDP } from "@app/components/LoginForm"; import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import { build } from "@server/build"; import { headers } from "next/headers"; -import { - LoadLoginPageResponse -} from "@server/routers/loginPage/types"; +import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; import { Card, diff --git a/src/app/auth/2fa/setup/page.tsx b/src/app/auth/2fa/setup/page.tsx index 64a6cf57..944731b9 100644 --- a/src/app/auth/2fa/setup/page.tsx +++ b/src/app/auth/2fa/setup/page.tsx @@ -45,7 +45,9 @@ export default function Setup2FAPage() { {t("otpSetup")} - {t("adminEnabled2FaOnYourAccount", { email: email || "your account" })} + {t("adminEnabled2FaOnYourAccount", { + email: email || "your account" + })} diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx index a2432e3e..3d646084 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx @@ -25,15 +25,17 @@ export default async function Page(props: { const allCookies = await cookies(); const stateCookie = allCookies.get("p_oidc_state")?.value; - const idpRes = await cache( - async () => await priv.get>(`/idp/${params.idpId}`) + async () => + await priv.get>( + `/idp/${params.idpId}` + ) )(); const foundIdp = idpRes.data?.data?.idp; if (!foundIdp) { - return
{t('idpErrorNotFound')}
; + return
{t("idpErrorNotFound")}
; } const allHeaders = await headers(); diff --git a/src/app/auth/login/device/page.tsx b/src/app/auth/login/device/page.tsx index 07c804fb..2fa5fac5 100644 --- a/src/app/auth/login/device/page.tsx +++ b/src/app/auth/login/device/page.tsx @@ -19,7 +19,9 @@ export default async function DeviceLoginPage({ searchParams }: Props) { const redirectDestination = code ? `/auth/login/device?code=${encodeURIComponent(code)}` : "/auth/login/device"; - redirect(`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectDestination)}`); + redirect( + `/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectDestination)}` + ); } const userName = user?.name || user?.username || ""; diff --git a/src/app/auth/login/device/success/page.tsx b/src/app/auth/login/device/success/page.tsx index 1b79c600..81e62fd6 100644 --- a/src/app/auth/login/device/success/page.tsx +++ b/src/app/auth/login/device/success/page.tsx @@ -26,7 +26,9 @@ export default function DeviceAuthSuccessPage() {
-

{t("deviceActivation")}

+

+ {t("deviceActivation")} +

diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 615f33cf..f632a88c 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -98,7 +98,11 @@ export default async function Page(props: {
)} - + {(!signUpDisabled || isInvite) && (

diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index 986c52e4..b6afd6d8 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -88,18 +88,18 @@ export default function ResetPasswordForm({ const formSchema = z .object({ - email: z.email({ message: t('emailInvalid') }), - token: z.string().min(8, { message: t('tokenInvalid') }), + email: z.email({ message: t("emailInvalid") }), + token: z.string().min(8, { message: t("tokenInvalid") }), password: passwordSchema, confirmPassword: passwordSchema }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], - message: t('passwordNotMatch') + message: t("passwordNotMatch") }); const mfaSchema = z.object({ - code: z.string().length(6, { message: t('pincodeInvalid') }) + code: z.string().length(6, { message: t("pincodeInvalid") }) }); const form = useForm({ @@ -139,8 +139,8 @@ export default function ResetPasswordForm({ } as RequestPasswordResetBody ) .catch((e) => { - setError(formatAxiosError(e, t('errorOccurred'))); - console.error(t('passwordErrorRequestReset'), e); + setError(formatAxiosError(e, t("errorOccurred"))); + console.error(t("passwordErrorRequestReset"), e); setIsSubmitting(false); }); @@ -169,8 +169,8 @@ export default function ResetPasswordForm({ } as ResetPasswordBody ) .catch((e) => { - setError(formatAxiosError(e, t('errorOccurred'))); - console.error(t('passwordErrorReset'), e); + setError(formatAxiosError(e, t("errorOccurred"))); + console.error(t("passwordErrorReset"), e); setIsSubmitting(false); }); @@ -186,7 +186,11 @@ export default function ResetPasswordForm({ return; } - setSuccessMessage(quickstart ? t('accountSetupSuccess') : t('passwordResetSuccess')); + setSuccessMessage( + quickstart + ? t("accountSetupSuccess") + : t("passwordResetSuccess") + ); // Auto-login after successful password reset try { @@ -208,7 +212,10 @@ export default function ResetPasswordForm({ try { await api.post("/auth/verify-email/request"); } catch (verificationError) { - console.error("Failed to send verification code:", verificationError); + console.error( + "Failed to send verification code:", + verificationError + ); } if (redirect) { @@ -229,7 +236,6 @@ export default function ResetPasswordForm({ } setIsSubmitting(false); }, 1500); - } catch (loginError) { // Auto-login failed, but password reset was successful console.error("Auto-login failed:", loginError); @@ -251,13 +257,14 @@ export default function ResetPasswordForm({ - {quickstart ? t('completeAccountSetup') : t('passwordReset')} + {quickstart + ? t("completeAccountSetup") + : t("passwordReset")} {quickstart - ? t('completeAccountSetupDescription') - : t('passwordResetDescription') - } + ? t("completeAccountSetupDescription") + : t("passwordResetDescription")} @@ -276,16 +283,19 @@ export default function ResetPasswordForm({ name="email" render={({ field }) => ( - {t('email')} + + {t("email")} + {quickstart - ? t('accountSetupSent') - : t('passwordResetSent') - } + ? t("accountSetupSent") + : t( + "passwordResetSent" + )} )} @@ -306,7 +316,9 @@ export default function ResetPasswordForm({ name="email" render={({ field }) => ( - {t('email')} + + {t("email")} + {quickstart - ? t('accountSetupCode') - : t('passwordResetCode') - } + ? t( + "accountSetupCode" + ) + : t( + "passwordResetCode" + )} {quickstart - ? t('accountSetupCodeDescription') - : t('passwordResetCodeDescription') - } + ? t( + "accountSetupCodeDescription" + ) + : t( + "passwordResetCodeDescription" + )} )} @@ -355,9 +373,8 @@ export default function ResetPasswordForm({ {quickstart - ? t('passwordCreate') - : t('passwordNew') - } + ? t("passwordCreate") + : t("passwordNew")} {quickstart - ? t('passwordCreateConfirm') - : t('passwordNewConfirm') - } + ? t( + "passwordCreateConfirm" + ) + : t( + "passwordNewConfirm" + )} ( - {t('pincodeAuth')} + {t("pincodeAuth")}

@@ -475,8 +495,10 @@ export default function ResetPasswordForm({ )} {state === "reset" - ? (quickstart ? t('completeSetup') : t('passwordReset')) - : t('pincodeSubmit2')} + ? quickstart + ? t("completeSetup") + : t("passwordReset") + : t("pincodeSubmit2")} )} @@ -491,9 +513,8 @@ export default function ResetPasswordForm({ )} {quickstart - ? t('accountSetupSubmit') - : t('passwordResetSubmit') - } + ? t("accountSetupSubmit") + : t("passwordResetSubmit")} )} @@ -507,7 +528,7 @@ export default function ResetPasswordForm({ mfaForm.reset(); }} > - {t('passwordBack')} + {t("passwordBack")} )} @@ -521,7 +542,7 @@ export default function ResetPasswordForm({ form.reset(); }} > - {t('backToEmail')} + {t("backToEmail")} )}
diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx index c549abf0..e4428370 100644 --- a/src/app/auth/verify-email/page.tsx +++ b/src/app/auth/verify-email/page.tsx @@ -35,10 +35,7 @@ export default async function Page(props: { return ( <> - + ); } diff --git a/src/app/globals.css b/src/app/globals.css index 221e228e..a2a98a2c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -54,7 +54,7 @@ --muted-foreground: oklch(0.705 0.015 286.067); --accent: oklch(0.274 0.006 286.033); --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); + --destructive: oklch(0.5382 0.1949 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.646 0.222 41.116); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 022a4d7b..36d5d2ce 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,19 +25,7 @@ import { TailwindIndicator } from "@app/components/TailwindIndicator"; export const metadata: Metadata = { title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, - description: "", - - ...(process.env.BRANDING_FAVICON_PATH - ? { - icons: { - icon: [ - { - url: process.env.BRANDING_FAVICON_PATH as string - } - ] - } - } - : {}) + description: "" }; export const dynamic = "force-dynamic"; diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index fbccc042..fabc24b7 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -60,7 +60,8 @@ export const orgNavSections = (): SidebarNavSection[] => [ { title: "sidebarClientResources", href: "/{orgId}/settings/resources/client", - icon: + icon: , + isBeta: true } ] }, @@ -104,7 +105,7 @@ export const orgNavSections = (): SidebarNavSection[] => [ ] }, { - heading: "accessControls", + heading: "access", items: [ { title: "sidebarUsers", diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 60c02bee..d3ca37cc 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,17 +1,16 @@ import { getTranslations } from "next-intl/server"; export default async function NotFound() { - const t = await getTranslations(); return (

404

- {t('pageNotFound')} + {t("pageNotFound")}

- {t('pageNotFoundDescription')} + {t("pageNotFoundDescription")}

); diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 53072554..36853e5c 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -312,7 +312,9 @@ export default function StepperForm() {
- {t("setupSubnetDescription")} + {t( + "setupSubnetDescription" + )}
)} diff --git a/src/components/AccessPageHeaderAndNav.tsx b/src/components/AccessPageHeaderAndNav.tsx index 47690dc6..12a34f06 100644 --- a/src/components/AccessPageHeaderAndNav.tsx +++ b/src/components/AccessPageHeaderAndNav.tsx @@ -14,21 +14,21 @@ export default function AccessPageHeaderAndNav({ hasInvitations }: AccessPageHeaderAndNavProps) { const t = useTranslations(); - + const navItems = [ { - title: t('users'), + title: t("users"), href: `/{orgId}/settings/access/users` }, { - title: t('roles'), + title: t("roles"), href: `/{orgId}/settings/access/roles` } ]; if (hasInvitations) { navItems.push({ - title: t('invite'), + title: t("invite"), href: `/{orgId}/settings/access/invitations` }); } @@ -36,13 +36,11 @@ export default function AccessPageHeaderAndNav({ return ( <> - - {children} - + {children} ); } diff --git a/src/components/AccessToken.tsx b/src/components/AccessToken.tsx index 969a2d4e..54f92643 100644 --- a/src/components/AccessToken.tsx +++ b/src/components/AccessToken.tsx @@ -20,10 +20,7 @@ type AccessTokenProps = { resourceId?: number; }; -export default function AccessToken({ - token, - resourceId -}: AccessTokenProps) { +export default function AccessToken({ token, resourceId }: AccessTokenProps) { const [loading, setLoading] = useState(true); const [isValid, setIsValid] = useState(false); @@ -79,7 +76,7 @@ export default function AccessToken({ ); } } catch (e) { - console.error(t('accessTokenError'), e); + console.error(t("accessTokenError"), e); } finally { setLoading(false); } @@ -102,7 +99,7 @@ export default function AccessToken({ ); } } catch (e) { - console.error(t('accessTokenError'), e); + console.error(t("accessTokenError"), e); } finally { setLoading(false); } @@ -118,26 +115,22 @@ export default function AccessToken({ function renderTitle() { if (isValid) { - return t('accessGranted'); + return t("accessGranted"); } else { - return t('accessUrlInvalid'); + return t("accessUrlInvalid"); } } function renderContent() { if (isValid) { - return ( -
- {t('accessGrantedDescription')} -
- ); + return
{t("accessGrantedDescription")}
; } else { return (
- {t('accessUrlInvalidDescription')} + {t("accessUrlInvalidDescription")}
diff --git a/src/components/AccessTokenUsage.tsx b/src/components/AccessTokenUsage.tsx index c44f43b7..4b170371 100644 --- a/src/components/AccessTokenUsage.tsx +++ b/src/components/AccessTokenUsage.tsx @@ -44,31 +44,35 @@ export default function AccessTokenSection({ <>

- {t('shareTokenDescription')} + {t("shareTokenDescription")}

- {t('accessToken')} - {t('usageExamples')} + {t("accessToken")} + + {t("usageExamples")} +
-
{t('tokenId')}
+
{t("tokenId")}
-
{t('token')}
+
{t("token")}
-

{t('requestHeades')}

+

+ {t("requestHeades")} +

-

{t('queryParameter')}

+

+ {t("queryParameter")} +

@@ -85,17 +91,17 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`} - {t('importantNote')} + {t("importantNote")} - {t('shareImportantDescription')} + {t("shareImportantDescription")}
- {t('shareTokenSecurety')} + {t("shareTokenSecurety")}
); diff --git a/src/components/AdminIdpDataTable.tsx b/src/components/AdminIdpDataTable.tsx index 26ec6b31..e11ed8fc 100644 --- a/src/components/AdminIdpDataTable.tsx +++ b/src/components/AdminIdpDataTable.tsx @@ -26,10 +26,10 @@ export function IdpDataTable({ columns={columns} data={data} persistPageSize="idp-table" - title={t('idp')} - searchPlaceholder={t('idpSearch')} + title={t("idp")} + searchPlaceholder={t("idpSearch")} searchColumn="name" - addButtonText={t('idpAdd')} + addButtonText={t("idpAdd")} onAdd={() => { router.push("/admin/idp/create"); }} diff --git a/src/components/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx index b0782fcb..76a0fdd7 100644 --- a/src/components/AdminIdpTable.tsx +++ b/src/components/AdminIdpTable.tsx @@ -175,9 +175,7 @@ export default function IdpTable({ idps }: Props) { - diff --git a/src/components/AdminUsersDataTable.tsx b/src/components/AdminUsersDataTable.tsx index 2eb3c2e2..afa473e8 100644 --- a/src/components/AdminUsersDataTable.tsx +++ b/src/components/AdminUsersDataTable.tsx @@ -1,8 +1,6 @@ "use client"; -import { - ColumnDef, -} from "@tanstack/react-table"; +import { ColumnDef } from "@tanstack/react-table"; import { DataTable } from "@app/components/ui/data-table"; import { useTranslations } from "next-intl"; @@ -19,7 +17,6 @@ export function UsersDataTable({ onRefresh, isRefreshing }: DataTableProps) { - const t = useTranslations(); return ( @@ -27,8 +24,8 @@ export function UsersDataTable({ columns={columns} data={data} persistPageSize="userServer-table" - title={t('userServer')} - searchPlaceholder={t('userSearch')} + title={t("userServer")} + searchPlaceholder={t("userSearch")} searchColumn="email" onRefresh={onRefresh} isRefreshing={isRefreshing} diff --git a/src/components/ApiKeysDataTable.tsx b/src/components/ApiKeysDataTable.tsx index c4f93136..8069b101 100644 --- a/src/components/ApiKeysDataTable.tsx +++ b/src/components/ApiKeysDataTable.tsx @@ -44,7 +44,6 @@ export function ApiKeysDataTable({ onRefresh, isRefreshing }: DataTableProps) { - const t = useTranslations(); return ( @@ -52,11 +51,11 @@ export function ApiKeysDataTable({ columns={columns} data={data} persistPageSize="apiKeys-table" - title={t('apiKeys')} - searchPlaceholder={t('searchApiKeys')} + title={t("apiKeys")} + searchPlaceholder={t("searchApiKeys")} searchColumn="name" onAdd={addApiKey} - addButtonText={t('apiKeysAdd')} + addButtonText={t("apiKeysAdd")} onRefresh={onRefresh} isRefreshing={isRefreshing} enableColumnVisibility={true} diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx index 863d2bc6..c3202277 100644 --- a/src/components/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -108,7 +108,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { { accessorKey: "key", friendlyName: t("key"), - header: () => ({t("key")}), + header: () => {t("key")}, cell: ({ row }) => { const r = row.original; return {r.key}; @@ -117,7 +117,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { { accessorKey: "createdAt", friendlyName: t("createdAt"), - header: () => ({t("createdAt")}), + header: () => {t("createdAt")}, cell: ({ row }) => { const r = row.original; return {moment(r.createdAt).format("lll")} ; @@ -161,9 +161,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { - diff --git a/src/components/AutoLoginHandler.tsx b/src/components/AutoLoginHandler.tsx index 2391ece6..ee8f4126 100644 --- a/src/components/AutoLoginHandler.tsx +++ b/src/components/AutoLoginHandler.tsx @@ -67,7 +67,8 @@ export default function AutoLoginHandler({ console.error("Failed to generate OIDC URL:", e); setError( t("autoLoginErrorGeneratingUrl", { - defaultValue: "An unexpected error occurred. Please try again." + defaultValue: + "An unexpected error occurred. Please try again." }) ); } finally { diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx index 540b8e0e..b4472365 100644 --- a/src/components/BrandingLogo.tsx +++ b/src/components/BrandingLogo.tsx @@ -38,7 +38,7 @@ export default function BrandingLogo(props: BrandingLogoProps) { if (isUnlocked() && env.branding.logo?.darkPath) { return env.branding.logo.darkPath; } - return "/logo/word_mark_white.png"; + return "/logo/word_mark_white.png"; } const path = getPath(); diff --git a/src/components/ChangePasswordDialog.tsx b/src/components/ChangePasswordDialog.tsx index 85a55ab2..3430f26a 100644 --- a/src/components/ChangePasswordDialog.tsx +++ b/src/components/ChangePasswordDialog.tsx @@ -20,7 +20,10 @@ type ChangePasswordDialogProps = { setOpen: (val: boolean) => void; }; -export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDialogProps) { +export default function ChangePasswordDialog({ + open, + setOpen +}: ChangePasswordDialogProps) { const t = useTranslations(); const [currentStep, setCurrentStep] = useState(1); const [loading, setLoading] = useState(false); @@ -47,18 +50,16 @@ export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDi > - - {t('changePassword')} - + {t("changePassword")} - {t('changePasswordDescription')} + {t("changePasswordDescription")} setOpen(false)} @@ -77,7 +78,7 @@ export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDi disabled={loading} onClick={handleSubmit} > - {t('submit')} + {t("submit")} )} diff --git a/src/components/ChangePasswordForm.tsx b/src/components/ChangePasswordForm.tsx index 5d1395bc..57015712 100644 --- a/src/components/ChangePasswordForm.tsx +++ b/src/components/ChangePasswordForm.tsx @@ -22,11 +22,7 @@ import { import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { useTranslations } from "next-intl"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot -} from "./ui/input-otp"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp"; import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; import { ChangePasswordResponse } from "@server/routers/auth"; import { cn } from "@app/lib/cn"; @@ -114,14 +110,22 @@ const ChangePasswordForm = forwardRef< onLoadingChange?.(loading); }, [loading, onLoadingChange]); - const passwordSchema = z.object({ - oldPassword: z.string().min(1, { message: t("passwordRequired") }), - newPassword: z.string().min(8, { message: t("passwordRequirementsChars") }), - confirmPassword: z.string().min(1, { message: t("passwordRequired") }) - }).refine((data) => data.newPassword === data.confirmPassword, { - message: t("passwordsDoNotMatch"), - path: ["confirmPassword"], - }); + const passwordSchema = z + .object({ + oldPassword: z + .string() + .min(1, { message: t("passwordRequired") }), + newPassword: z + .string() + .min(8, { message: t("passwordRequirementsChars") }), + confirmPassword: z + .string() + .min(1, { message: t("passwordRequired") }) + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: t("passwordsDoNotMatch"), + path: ["confirmPassword"] + }); const mfaSchema = z.object({ code: z.string().length(6, { message: t("pincodeInvalid") }) @@ -143,11 +147,13 @@ const ChangePasswordForm = forwardRef< } }); - const changePassword = async (values: z.infer) => { + const changePassword = async ( + values: z.infer + ) => { setLoading(true); const endpoint = `/auth/change-password`; - const payload = { + const payload = { oldPassword: values.oldPassword, newPassword: values.newPassword }; @@ -181,7 +187,7 @@ const ChangePasswordForm = forwardRef< const endpoint = `/auth/change-password`; const passwordValues = passwordForm.getValues(); - const payload = { + const payload = { oldPassword: passwordValues.oldPassword, newPassword: passwordValues.newPassword, code: values.code @@ -303,7 +309,9 @@ const ChangePasswordForm = forwardRef<
- {t("passwordStrength")} + {t( + "passwordStrength" + )}
- {t("passwordRequirements")} + {t( + "passwordRequirements" + )}
@@ -505,13 +515,14 @@ const ChangePasswordForm = forwardRef< {confirmPasswordValue.length > 0 && !doPasswordsMatch && (

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

)} {/* Only show FormMessage when field is empty */} - {confirmPasswordValue.length === 0 && ( - - )} + {confirmPasswordValue.length === + 0 && } )} /> @@ -523,7 +534,9 @@ const ChangePasswordForm = forwardRef< {step === 2 && (
-

{t("otpAuth")}

+

+ {t("otpAuth")} +

{t("otpAuthDescription")}

@@ -551,9 +564,12 @@ const ChangePasswordForm = forwardRef< onChange={( value: string ) => { - field.onChange(value); + field.onChange( + value + ); if ( - value.length === 6 + value.length === + 6 ) { mfaForm.handleSubmit( confirmMfa @@ -630,10 +646,7 @@ const ChangePasswordForm = forwardRef< )} {step === 3 && ( - )} @@ -644,4 +657,4 @@ const ChangePasswordForm = forwardRef< } ); -export default ChangePasswordForm; \ No newline at end of file +export default ChangePasswordForm; diff --git a/src/components/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx index f8d96158..8e7fa5e7 100644 --- a/src/components/ClientInfoCard.tsx +++ b/src/components/ClientInfoCard.tsx @@ -19,25 +19,27 @@ export default function SiteInfoCard({}: ClientInfoCardProps) { return ( - - <> - - {t("status")} - - {client.online ? ( -
-
- {t("online")} -
- ) : ( -
-
- {t("offline")} -
- )} -
-
- + + + {t("identifier")} + {client.niceId} + + + {t("status")} + + {client.online ? ( +
+
+ {t("online")} +
+ ) : ( +
+
+ {t("offline")} +
+ )} +
+
{t("address")} diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index e813cb57..a5e257c7 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -25,32 +25,6 @@ import EditInternalResourceDialog from "@app/components/EditInternalResourceDial import { orgQueries } from "@app/lib/queries"; import { useQuery } from "@tanstack/react-query"; -export type TargetHealth = { - targetId: number; - ip: string; - port: number; - enabled: boolean; - healthStatus?: "healthy" | "unhealthy" | "unknown"; -}; - -export type ResourceRow = { - id: number; - nice: string | null; - name: string; - orgId: string; - domain: string; - authState: string; - http: boolean; - protocol: string; - proxyPort: number | null; - enabled: boolean; - domainId?: string; - ssl: boolean; - targetHost?: string; - targetPort?: number; - targets?: TargetHealth[]; -}; - export type InternalResourceRow = { id: number; name: string; @@ -66,6 +40,7 @@ export type InternalResourceRow = { destination: string; // destinationPort: number | null; alias: string | null; + niceId: string; }; type ClientResourcesTableProps = { @@ -158,6 +133,28 @@ export default function ClientResourcesTable({ ); } }, + { + id: "niceId", + accessorKey: "niceId", + friendlyName: t("identifier"), + enableHiding: true, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return {row.original.niceId || "-"}; + } + }, { accessorKey: "siteName", friendlyName: t("site"), diff --git a/src/components/ClientsDataTable.tsx b/src/components/ClientsDataTable.tsx index cf42e7bc..8d38e597 100644 --- a/src/components/ClientsDataTable.tsx +++ b/src/components/ClientsDataTable.tsx @@ -1,8 +1,6 @@ "use client"; -import { - ColumnDef, -} from "@tanstack/react-table"; +import { ColumnDef } from "@tanstack/react-table"; import { DataTable } from "@app/components/ui/data-table"; type TabFilter = { diff --git a/src/components/ColumnFilter.tsx b/src/components/ColumnFilter.tsx index eee91ecc..a856984e 100644 --- a/src/components/ColumnFilter.tsx +++ b/src/components/ColumnFilter.tsx @@ -1,7 +1,18 @@ import { useState } from "react"; import { Button } from "@app/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; import { CheckIcon, ChevronDownIcon, Filter } from "lucide-react"; import { cn } from "@app/lib/cn"; @@ -31,7 +42,9 @@ export function ColumnFilter({ }: ColumnFilterProps) { const [open, setOpen] = useState(false); - const selectedOption = options.find(option => option.value === selectedValue); + const selectedOption = options.find( + (option) => option.value === selectedValue + ); return ( @@ -49,7 +62,9 @@ export function ColumnFilter({
- {selectedOption ? selectedOption.label : placeholder} + {selectedOption + ? selectedOption.label + : placeholder}
@@ -79,7 +94,9 @@ export function ColumnFilter({ value={option.label} onSelect={() => { onValueChange( - selectedValue === option.value ? undefined : option.value + selectedValue === option.value + ? undefined + : option.value ); setOpen(false); }} @@ -101,4 +118,4 @@ export function ColumnFilter({
); -} \ No newline at end of file +} diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx index 7a5cc74b..5a0f49f9 100644 --- a/src/components/ContainersSelector.tsx +++ b/src/components/ContainersSelector.tsx @@ -186,7 +186,7 @@ const DockerContainersTable: FC<{ { accessorKey: "name", friendlyName: t("containerName"), - header: () => ({t("containerName")}), + header: () => {t("containerName")}, cell: ({ row }) => (
{row.original.name}
) @@ -194,7 +194,7 @@ const DockerContainersTable: FC<{ { accessorKey: "image", friendlyName: t("containerImage"), - header: () => ({t("containerImage")}), + header: () => {t("containerImage")}, cell: ({ row }) => (
{row.original.image} @@ -204,7 +204,7 @@ const DockerContainersTable: FC<{ { accessorKey: "state", friendlyName: t("containerState"), - header: () => ({t("containerState")}), + header: () => {t("containerState")}, cell: ({ row }) => ( ({t("containerNetworks")}), + header: () => {t("containerNetworks")}, cell: ({ row }) => { const networks = Object.keys(row.original.networks); return ( @@ -239,7 +239,9 @@ const DockerContainersTable: FC<{ { accessorKey: "hostname", friendlyName: t("containerHostnameIp"), - header: () => ({t("containerHostnameIp")}), + header: () => ( + {t("containerHostnameIp")} + ), enableHiding: false, cell: ({ row }) => (
@@ -250,7 +252,7 @@ const DockerContainersTable: FC<{ { accessorKey: "labels", friendlyName: t("containerLabels"), - header: () => ({t("containerLabels")}), + header: () => {t("containerLabels")}, cell: ({ row }) => { const labels = row.original.labels || {}; const labelEntries = Object.entries(labels); @@ -302,7 +304,7 @@ const DockerContainersTable: FC<{ }, { accessorKey: "ports", - header: () => ({t("containerPorts")}), + header: () => {t("containerPorts")}, enableHiding: false, cell: ({ row }) => { const ports = getExposedPorts(row.original); @@ -360,7 +362,7 @@ const DockerContainersTable: FC<{ }, { id: "actions", - header: () => ({t("containerActions")}), + header: () => {t("containerActions")}, cell: ({ row }) => { const ports = getExposedPorts(row.original); return ( diff --git a/src/components/CopyTextBox.tsx b/src/components/CopyTextBox.tsx index 72a99c3f..e516bf0d 100644 --- a/src/components/CopyTextBox.tsx +++ b/src/components/CopyTextBox.tsx @@ -29,7 +29,7 @@ export default function CopyTextBox({ setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } catch (err) { - console.error(t('copyTextFailed'), err); + console.error(t("copyTextFailed"), err); } } }; @@ -54,7 +54,7 @@ export default function CopyTextBox({ type="button" className="absolute top-0.5 right-0 z-10 bg-card" onClick={copyToClipboard} - aria-label={t('copyTextClipboard')} + aria-label={t("copyTextClipboard")} > {isCopied ? ( diff --git a/src/components/CopyToClipboard.tsx b/src/components/CopyToClipboard.tsx index 5cdeed55..b755f9a5 100644 --- a/src/components/CopyToClipboard.tsx +++ b/src/components/CopyToClipboard.tsx @@ -9,7 +9,11 @@ type CopyToClipboardProps = { isLink?: boolean; }; -const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) => { +const CopyToClipboard = ({ + text, + displayText, + isLink +}: CopyToClipboardProps) => { const [copied, setCopied] = useState(false); const handleCopy = () => { @@ -60,7 +64,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) => ) : ( )} - {t('copyText')} + {t("copyText")}
); diff --git a/src/components/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx index 7d614376..9a187f1e 100644 --- a/src/components/CreateDomainForm.tsx +++ b/src/components/CreateDomainForm.tsx @@ -45,11 +45,16 @@ import { } from "@app/components/InfoSection"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { build } from "@server/build"; -import { toASCII, toUnicode } from 'punycode'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; +import { toASCII, toUnicode } from "punycode"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "./ui/select"; import { useRouter } from "next/navigation"; - // Helper functions for Unicode domain handling function toPunycode(domain: string): string { try { @@ -76,9 +81,9 @@ function isValidDomainFormat(domain: string): boolean { return false; } - const parts = domain.split('.'); + const parts = domain.split("."); for (const part of parts) { - if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) { + if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) { return false; } if (part.length > 63) { @@ -137,7 +142,8 @@ export default function CreateDomainForm({ resolver: zodResolver(formSchema), defaultValues: { baseDomain: "", - type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns", + type: + build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns", certResolver: null, preferWildcardCert: false } @@ -172,7 +178,9 @@ export default function CreateDomainForm({ description: t("domainCreatedDescription") }); onCreated?.(domainData); - router.push(`/${org.org.orgId}/settings/domains/${domainData.domainId}`); + router.push( + `/${org.org.orgId}/settings/domains/${domainData.domainId}` + ); } catch (e) { toast({ title: t("error"), @@ -182,7 +190,7 @@ export default function CreateDomainForm({ } finally { setLoading(false); } - }; + } // Domain type options let domainOptions: any = []; @@ -225,145 +233,213 @@ export default function CreateDomainForm({ -
- - ( - - - - - )} - /> - ( - - {t("domain")} - - - - {punycodePreview && ( - - - - {t("internationaldomaindetected")} - -
-

{t("willbestoredas")} {punycodePreview}

-
-
-
-
- )} - -
- )} - /> - {domainType === "wildcard" && ( - <> - ( - - {t("certResolver")} - - - - - - )} + + + ( + + - {form.watch("certResolver") !== null && - form.watch("certResolver") !== "default" && ( - ( - - - field.onChange(e.target.value)} - /> - - - - )} - /> - )} + + + )} + /> + ( + + {t("domain")} + + + + {punycodePreview && ( + + + + + {t( + "internationaldomaindetected" + )} + + +
+

+ {t( + "willbestoredas" + )}{" "} + + { + punycodePreview + } + +

+
+
+
+
+ )} + +
+ )} + /> + {domainType === "wildcard" && ( + <> + ( + + + {t("certResolver")} + + + + + + + )} + /> + {form.watch("certResolver") !== null && + form.watch("certResolver") !== + "default" && ( + ( + + + + field.onChange( + e.target + .value + ) + } + /> + + + + )} + /> + )} - {form.watch("certResolver") !== null && - form.watch("certResolver") !== "default" && ( - ( - - - - - {/*
+ {form.watch("certResolver") !== null && + form.watch("certResolver") !== + "default" && ( + ( + + + + + {/*
{t("preferWildcardCert")}
*/} -
- )} - /> - )} - - )} - - - + + )} + /> + )} + + )} + + diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 2e7ee2dc..91ef26da 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -232,6 +232,21 @@ export default function CreateInternalResourceDialog({ const mode = form.watch("mode"); + // Helper function to check if destination contains letters (hostname vs IP) + const isHostname = (destination: string): boolean => { + return /[a-zA-Z]/.test(destination); + }; + + // Helper function to clean resource name for FQDN format + const cleanForFQDN = (name: string): string => { + return name + .toLowerCase() + .replace(/[^a-z0-9.-]/g, "-") // Replace invalid chars with hyphens + .replace(/[-]+/g, "-") // Replace multiple hyphens with single hyphen + .replace(/^-|-$/g, "") // Remove leading/trailing hyphens + .replace(/^\.|\.$/g, ""); // Remove leading/trailing dots + }; + useEffect(() => { if (open && availableSites.length > 0) { form.reset({ @@ -253,6 +268,26 @@ export default function CreateInternalResourceDialog({ const handleSubmit = async (data: FormData) => { setIsSubmitting(true); try { + // Validate: if mode is "host" and destination is a hostname (contains letters), + // an alias is required + if (data.mode === "host" && isHostname(data.destination)) { + const currentAlias = data.alias?.trim() || ""; + + if (!currentAlias) { + // Prefill alias based on destination + let aliasValue = data.destination; + if (data.destination.toLowerCase() === "localhost") { + // Use resource name cleaned for FQDN with .internal suffix + const cleanedName = cleanForFQDN(data.name); + aliasValue = `${cleanedName}.internal`; + } + + // Update the form with the prefilled alias + form.setValue("alias", aliasValue); + data.alias = aliasValue; + } + } + const response = await api.put>( `/org/${orgId}/site/${data.siteId}/resource`, { diff --git a/src/components/CreateRoleForm.tsx b/src/components/CreateRoleForm.tsx index dca75f5a..b8df1f78 100644 --- a/src/components/CreateRoleForm.tsx +++ b/src/components/CreateRoleForm.tsx @@ -48,7 +48,7 @@ export default function CreateRoleForm({ const t = useTranslations(); const formSchema = z.object({ - name: z.string({ message: t('nameRequired') }).max(32), + name: z.string({ message: t("nameRequired") }).max(32), description: z.string().max(255).optional() }); @@ -78,10 +78,10 @@ export default function CreateRoleForm({ .catch((e) => { toast({ variant: "destructive", - title: t('accessRoleErrorCreate'), + title: t("accessRoleErrorCreate"), description: formatAxiosError( e, - t('accessRoleErrorCreateDescription') + t("accessRoleErrorCreateDescription") ) }); }); @@ -89,8 +89,8 @@ export default function CreateRoleForm({ if (res && res.status === 201) { toast({ variant: "default", - title: t('accessRoleCreated'), - description: t('accessRoleCreatedDescription') + title: t("accessRoleCreated"), + description: t("accessRoleCreatedDescription") }); if (open) { @@ -117,9 +117,9 @@ export default function CreateRoleForm({ > - {t('accessRoleCreate')} + {t("accessRoleCreate")} - {t('accessRoleCreateDescription')} + {t("accessRoleCreateDescription")} @@ -134,7 +134,9 @@ export default function CreateRoleForm({ name="name" render={({ field }) => ( - {t('accessRoleName')} + + {t("accessRoleName")} + @@ -147,7 +149,9 @@ export default function CreateRoleForm({ name="description" render={({ field }) => ( - {t('description')} + + {t("description")} + @@ -160,7 +164,7 @@ export default function CreateRoleForm({ - + diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index 3cc203f3..361bfe7d 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -67,7 +67,7 @@ import { } from "@app/components/ui/collapsible"; import AccessTokenSection from "@app/components/AccessTokenUsage"; import { useTranslations } from "next-intl"; -import { toUnicode } from 'punycode'; +import { toUnicode } from "punycode"; type FormProps = { open: boolean; @@ -104,7 +104,7 @@ export default function CreateShareLinkForm({ >([]); const formSchema = z.object({ - resourceId: z.number({ message: t('shareErrorSelectResource') }), + resourceId: z.number({ message: t("shareErrorSelectResource") }), resourceName: z.string(), resourceUrl: z.string(), timeUnit: z.string(), @@ -113,12 +113,12 @@ export default function CreateShareLinkForm({ }); const timeUnits = [ - { unit: "minutes", name: t('minutes') }, - { unit: "hours", name: t('hours') }, - { unit: "days", name: t('days') }, - { unit: "weeks", name: t('weeks') }, - { unit: "months", name: t('months') }, - { unit: "years", name: t('years') } + { unit: "minutes", name: t("minutes") }, + { unit: "hours", name: t("hours") }, + { unit: "days", name: t("days") }, + { unit: "weeks", name: t("weeks") }, + { unit: "months", name: t("months") }, + { unit: "years", name: t("years") } ]; const form = useForm>({ @@ -144,10 +144,10 @@ export default function CreateShareLinkForm({ console.error(e); toast({ variant: "destructive", - title: t('shareErrorFetchResource'), + title: t("shareErrorFetchResource"), description: formatAxiosError( e, - t('shareErrorFetchResourceDescription') + t("shareErrorFetchResourceDescription") ) }); }); @@ -204,17 +204,21 @@ export default function CreateShareLinkForm({ validForSeconds: neverExpire ? undefined : timeInSeconds, title: values.title || - t('shareLink', {resource: (values.resourceName || "Resource" + values.resourceId)}) + t("shareLink", { + resource: + values.resourceName || + "Resource" + values.resourceId + }) } ) .catch((e) => { console.error(e); toast({ variant: "destructive", - title: t('shareErrorCreate'), + title: t("shareErrorCreate"), description: formatAxiosError( e, - t('shareErrorCreateDescription') + t("shareErrorCreateDescription") ) }); }); @@ -263,9 +267,9 @@ export default function CreateShareLinkForm({ > - {t('shareCreate')} + {t("shareCreate")} - {t('shareCreateDescription')} + {t("shareCreateDescription")} @@ -283,7 +287,7 @@ export default function CreateShareLinkForm({ render={({ field }) => ( - {t('resource')} + {t("resource")} @@ -301,17 +305,25 @@ export default function CreateShareLinkForm({ ? getSelectedResourceName( field.value ) - : t('resourceSelect')} + : t( + "resourceSelect" + )} - + - {t('resourcesNotFound')} + {t( + "resourcesNotFound" + )} {resources.map( @@ -367,7 +379,9 @@ export default function CreateShareLinkForm({ render={({ field }) => ( - {t('shareTitleOptional')} + {t( + "shareTitleOptional" + )} @@ -379,7 +393,9 @@ export default function CreateShareLinkForm({
- {t('expireIn')} + + {t("expireIn")} +
- + @@ -458,12 +480,12 @@ export default function CreateShareLinkForm({ htmlFor="terms" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > - {t('neverExpire')} + {t("neverExpire")}

- {t('shareExpireDescription')} + {t("shareExpireDescription")}

@@ -471,16 +493,15 @@ export default function CreateShareLinkForm({ )} {link && (
-

- {t('shareSeeOnce')} -

-

- {t('shareAccessHint')} -

+

{t("shareSeeOnce")}

+

{t("shareAccessHint")}

- +
@@ -503,12 +524,12 @@ export default function CreateShareLinkForm({ className="p-0 flex items-center justify-between w-full" >

- {t('shareTokenUsage')} + {t("shareTokenUsage")}

- {t('toggle')} + {t("toggle")}
@@ -538,7 +559,7 @@ export default function CreateShareLinkForm({ - + diff --git a/src/components/Credenza.tsx b/src/components/Credenza.tsx index 6b80b3d3..8176273f 100644 --- a/src/components/Credenza.tsx +++ b/src/components/Credenza.tsx @@ -78,7 +78,10 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => { const CredenzaClose = isDesktop ? DialogClose : DrawerClose; return ( - + {children} ); @@ -128,7 +131,7 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => { const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader; return ( - + {children} ); @@ -155,7 +158,13 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => { // ); return ( -
+
{children}
); @@ -168,7 +177,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => { const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter; return ( - + {children} ); diff --git a/src/components/DNSRecordTable.tsx b/src/components/DNSRecordTable.tsx index bc764eb9..6995301d 100644 --- a/src/components/DNSRecordTable.tsx +++ b/src/components/DNSRecordTable.tsx @@ -21,10 +21,7 @@ type Props = { type: string | null; }; -export default function DNSRecordsTable({ - records, - type -}: Props) { +export default function DNSRecordsTable({ records, type }: Props) { const t = useTranslations(); const env = useEnvContext(); @@ -114,11 +111,5 @@ export default function DNSRecordsTable({ ...(env.env.flags.usePangolinDns ? [statusColumn] : []) ]; - return ( - - ); + return ; } diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx index 2a6b75fd..786b2d71 100644 --- a/src/components/DNSRecordsDataTable.tsx +++ b/src/components/DNSRecordsDataTable.tsx @@ -110,7 +110,11 @@ export function DNSRecordsDataTable({

{t("dnsRecord")}

{t("required")}
- +